1use crate::authority_contracts::{RetrievalWitnessV1, StageOutcomeV1};
8use crate::origin_authority::{
9 AuthorityScopeV1, GovernedAccessRequestV1, OriginAuthorityRecordV1, OriginRiskV1,
10};
11use crate::state_epistemics::{
12 answer_policy_for, resolve_dependency_states, AnswerPolicyDecision, DependencyResolutionV1,
13 DependencyState, PremiseStatus, StateResolutionMode, StateResolutionReceiptV1,
14};
15use crate::types::{GraphEdgeType, SearchContext, SearchResult, SearchSource, SearchSourceType};
16use crate::{MemoryError, MemoryStore, StateView};
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeSet;
19
20pub const EVIDENCE_GAP_V1: &str = "evidence_gap_v1";
21pub const EVIDENCE_PACKET_V1: &str = "evidence_packet_v1";
22pub const EVIDENCE_ROUTE_RECEIPT_V1: &str = "evidence_route_receipt_v1";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum EvidenceTerminalOutcomeV1 {
27 Sufficient,
28 EvidenceInsufficient,
29 BudgetExceeded,
30 Conflict,
31 InvalidLineage,
32}
33
34pub type EvidenceGapOutcomeV1 = EvidenceTerminalOutcomeV1;
36pub type EvidenceTerminalOutcome = EvidenceTerminalOutcomeV1;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum EvidenceGapReasonV1 {
41 MissingRequiredEvidence,
42 MissingStateSupport,
43 MissingOriginAuthority,
44 NoCandidates,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct EvidenceGapV1 {
49 pub schema_version: String,
50 pub gap_id: String,
51 pub reason: EvidenceGapReasonV1,
52 pub requirement: String,
53 pub missing_items: Vec<String>,
54 pub diagnosed_by: Vec<String>,
55 pub follow_up_queries: Vec<String>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum EvidenceRetrievalRouteV1 {
61 InitialHybrid,
62 Lexical,
63 Vector,
64 Graph,
65 Episodic,
66 DirectId,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct EvidenceRouteReceiptV1 {
71 pub schema_version: String,
72 pub route: EvidenceRetrievalRouteV1,
73 pub query: Option<String>,
74 pub stage_outcome: StageOutcomeV1,
75 pub result_ids: Vec<String>,
76 pub order_changed: bool,
77 pub budget_unit: usize,
78 pub note: Option<String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct StateRerankWeightsV1 {
83 pub state_relevance: f64,
84 pub support: f64,
85 pub origin_authority: f64,
86}
87
88impl Default for StateRerankWeightsV1 {
89 fn default() -> Self {
90 Self {
91 state_relevance: 2.0,
94 support: 0.5,
95 origin_authority: 0.25,
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
101pub struct StateRerankCandidateV1 {
102 pub result: SearchResult,
103 pub state_status: PremiseStatus,
104 pub state_relevance: f64,
105 pub support_score: f64,
106 pub origin_authority_score: f64,
107 pub original_score: f64,
108 pub reranked_score: f64,
109 pub origin_label_digest: Option<String>,
110}
111
112impl StateRerankCandidateV1 {
113 pub fn from_result(
114 result: SearchResult,
115 state_status: PremiseStatus,
116 state_relevance: f64,
117 ) -> Self {
118 let support_score = match (result.bm25_rank.is_some(), result.vector_rank.is_some()) {
119 (true, true) => 1.0,
120 (true, false) | (false, true) => 0.7,
121 (false, false) => 0.25,
122 };
123 let original_score = result.score;
124 let state_relevance = state_relevance.min(state_relevance_for_status(state_status));
125 Self {
126 result,
127 state_status,
128 state_relevance,
129 support_score,
130 origin_authority_score: 0.5,
131 original_score,
132 reranked_score: original_score,
133 origin_label_digest: None,
134 }
135 }
136
137 fn with_origin(mut self, origin: Option<&OriginAuthorityRecordV1>) -> Self {
138 self.origin_label_digest = origin.map(|record| record.label_digest.clone());
139 self.origin_authority_score =
140 origin.map_or(0.0, |record| origin_authority_score(&record.label));
141 self
142 }
143}
144
145pub fn rerank_state_aware(
148 mut candidates: Vec<StateRerankCandidateV1>,
149 weights: StateRerankWeightsV1,
150) -> Vec<StateRerankCandidateV1> {
151 for candidate in &mut candidates {
152 candidate.reranked_score = candidate.original_score
153 + weights.state_relevance * candidate.state_relevance
154 + weights.support * candidate.support_score
155 + weights.origin_authority * candidate.origin_authority_score;
156 }
157 candidates.sort_by(|a, b| {
158 b.reranked_score.total_cmp(&a.reranked_score).then_with(|| {
159 a.result
160 .source
161 .result_id()
162 .cmp(&b.result.source.result_id())
163 })
164 });
165 candidates
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct EvidencePacketItemV1 {
170 pub result: SearchResult,
171 pub state_status: PremiseStatus,
172 pub state_relevance: f64,
173 pub support_score: f64,
174 pub origin_authority_score: f64,
175 pub original_score: f64,
176 pub reranked_score: f64,
177 pub origin_label_digest: Option<String>,
178}
179
180impl From<StateRerankCandidateV1> for EvidencePacketItemV1 {
181 fn from(value: StateRerankCandidateV1) -> Self {
182 Self {
183 result: value.result,
184 state_status: value.state_status,
185 state_relevance: value.state_relevance,
186 support_score: value.support_score,
187 origin_authority_score: value.origin_authority_score,
188 original_score: value.original_score,
189 reranked_score: value.reranked_score,
190 origin_label_digest: value.origin_label_digest,
191 }
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct EvidenceAblationReceiptV1 {
197 pub schema_version: String,
198 pub baseline_result_ids: Vec<String>,
199 pub gap_loop_result_ids: Vec<String>,
200 pub packet_digest_without_gap_loop: String,
201 pub packet_digest_with_gap_loop: String,
202 pub answer_without_gap_loop: Option<String>,
203 pub answer_with_gap_loop: Option<String>,
204 pub baseline_outcome: EvidenceTerminalOutcomeV1,
205 pub final_outcome: EvidenceTerminalOutcomeV1,
206 pub packet_changed: bool,
207 pub order_changed: bool,
208 pub answer_changed: bool,
209 pub improved: bool,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct EvidenceGapRequestV1 {
214 pub schema_version: String,
215 pub request_id: String,
216 pub query: String,
217 pub top_k: usize,
218 pub namespaces: Option<Vec<String>>,
219 pub state_mode: StateResolutionMode,
220 pub budget: usize,
221 pub follow_up_limit: usize,
222 pub required_evidence: Vec<String>,
223 pub direct_ids: Vec<String>,
224 pub access_request: Option<GovernedAccessRequestV1>,
225 pub rerank_weights: StateRerankWeightsV1,
226}
227
228impl EvidenceGapRequestV1 {
229 pub fn new(query: impl Into<String>, state_view: StateView) -> Self {
230 let state_mode = match state_view {
231 StateView::Current => StateResolutionMode::Current,
232 StateView::HistoricalAt(value) => StateResolutionMode::HistoricalAt(value),
233 StateView::RecordedAsOf(value) => StateResolutionMode::HistoricalAt(value),
234 StateView::IncludeSuperseded => StateResolutionMode::Trajectory { points: Vec::new() },
235 };
236 let query = query.into();
237 Self {
238 schema_version: EVIDENCE_GAP_V1.into(),
239 request_id: format!("evidence-gap:{}", uuid::Uuid::new_v4()),
240 query,
241 top_k: 8,
242 namespaces: None,
243 state_mode,
244 budget: 8,
245 follow_up_limit: 4,
246 required_evidence: Vec::new(),
247 direct_ids: Vec::new(),
248 access_request: None,
249 rerank_weights: StateRerankWeightsV1::default(),
250 }
251 }
252
253 pub fn from_mode(query: impl Into<String>, state_mode: StateResolutionMode) -> Self {
254 let mut request = Self::new(query, state_mode.state_view());
255 request.state_mode = state_mode;
256 request
257 }
258
259 pub fn with_required_evidence(mut self, evidence: Vec<String>) -> Self {
260 self.required_evidence = evidence;
261 self
262 }
263
264 pub fn with_access_request(mut self, request: GovernedAccessRequestV1) -> Self {
265 self.access_request = Some(request);
266 self
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct EvidencePacketV1 {
272 pub schema_version: String,
273 pub packet_id: String,
274 pub request_id: String,
275 pub query: String,
276 pub state_view: StateView,
277 pub outcome: EvidenceTerminalOutcomeV1,
278 pub items: Vec<EvidencePacketItemV1>,
279 pub gaps: Vec<EvidenceGapV1>,
280 pub routes: Vec<EvidenceRouteReceiptV1>,
281 pub answer: Option<String>,
282 pub answer_policy: AnswerPolicyDecision,
283 pub state_resolution_receipt: StateResolutionReceiptV1,
284 pub retrieval_witness: RetrievalWitnessV1,
285 pub ablation: EvidenceAblationReceiptV1,
286 pub packet_digest: String,
287}
288
289fn origin_authority_score(label: &crate::OriginAuthorityLabelV1) -> f64 {
290 if label.revocation_status != crate::RevocationStatusV1::Active {
291 return 0.0;
292 }
293 let risk = match label.risk {
294 OriginRiskV1::Low => 1.0,
295 OriginRiskV1::Medium => 0.75,
296 OriginRiskV1::High => 0.5,
297 OriginRiskV1::Critical => 0.25,
298 };
299 let scope = match label.scopes.recall {
300 AuthorityScopeV1::Universal => 1.0,
301 AuthorityScopeV1::Audience => 0.8,
302 AuthorityScopeV1::PrincipalOnly => 0.5,
303 AuthorityScopeV1::Denied => 0.0,
304 };
305 risk * scope
306}
307
308fn state_relevance(status: PremiseStatus) -> f64 {
309 state_relevance_for_status(status)
310}
311
312fn state_relevance_for_status(status: PremiseStatus) -> f64 {
313 match status {
314 PremiseStatus::Supported => 1.0,
315 PremiseStatus::Unsupported => 0.35,
316 PremiseStatus::Ambiguous => 0.15,
317 PremiseStatus::Stale => 0.05,
318 PremiseStatus::Contradicted => 0.0,
319 }
320}
321
322fn normalized(value: &str) -> String {
323 value
324 .split_whitespace()
325 .collect::<Vec<_>>()
326 .join(" ")
327 .to_lowercase()
328}
329
330fn digest<T: Serialize>(value: &T) -> Result<String, MemoryError> {
331 let bytes = serde_json::to_vec(value).map_err(|error| {
332 MemoryError::Other(format!("evidence digest serialization failed: {error}"))
333 })?;
334 Ok(format!("blake3:{}", blake3::hash(&bytes).to_hex()))
335}
336
337fn ids(results: &[SearchResult]) -> Vec<String> {
338 results
339 .iter()
340 .map(|result| result.source.result_id())
341 .collect()
342}
343
344fn state_status_for(result: &SearchResult, resolution: &DependencyResolutionV1) -> PremiseStatus {
345 match resolution.status(&result.source.result_id()) {
346 Some(DependencyState::Invalid) => PremiseStatus::Stale,
347 Some(DependencyState::Uncertain) => PremiseStatus::Ambiguous,
348 Some(DependencyState::Pending) => PremiseStatus::Unsupported,
349 Some(DependencyState::Valid) | None => PremiseStatus::Supported,
350 }
351}
352
353fn edge_from_stored(edge: &crate::StoredGraphEdge) -> Option<crate::StateDependencyEdgeV1> {
354 let graph_type = edge
355 .edge_type_parsed
356 .clone()
357 .or_else(|| serde_json::from_str(&edge.edge_type).ok())?;
358 let GraphEdgeType::Entity { relation } = graph_type else {
359 return None;
360 };
361 match relation.as_str() {
362 "invalidates" => Some(crate::StateDependencyEdgeV1::invalidates(
363 &edge.source,
364 &edge.target,
365 )),
366 "weakens" => Some(crate::StateDependencyEdgeV1::weakens(
367 &edge.source,
368 &edge.target,
369 )),
370 "requires_reevaluation" => Some(crate::StateDependencyEdgeV1::requires_reevaluation(
371 &edge.source,
372 &edge.target,
373 )),
374 "scope_changes" => Some(crate::StateDependencyEdgeV1::scope_changes(
375 &edge.source,
376 &edge.target,
377 "unknown",
378 "unknown",
379 )),
380 "derived_from_state" => Some(crate::StateDependencyEdgeV1::derived_from_state(
381 &edge.source,
382 &edge.target,
383 )),
384 _ => None,
385 }
386}
387
388fn required_satisfied(
389 results: &[SearchResult],
390 required: &[String],
391 retrieval_queries: &[String],
392) -> bool {
393 required.iter().all(|requirement| {
394 let needle = normalized(requirement);
395 results.iter().any(|result| {
396 normalized(&result.content).contains(&needle)
397 && retrieval_queries
398 .iter()
399 .any(|query| normalized(query).contains(&needle))
400 })
401 })
402}
403
404fn evidence_sufficient(
405 results: &[SearchResult],
406 required: &[String],
407 retrieval_queries: &[String],
408 resolution: &DependencyResolutionV1,
409) -> bool {
410 !results.is_empty()
411 && required_satisfied(results, required, retrieval_queries)
412 && results.iter().any(|result| {
413 matches!(
414 state_status_for(result, resolution),
415 PremiseStatus::Supported
416 )
417 })
418}
419
420fn diagnose_gaps(
421 results: &[SearchResult],
422 required: &[String],
423 retrieval_queries: &[String],
424 resolution: &DependencyResolutionV1,
425 origin_denied: bool,
426 query: &str,
427) -> Vec<EvidenceGapV1> {
428 let mut gaps = Vec::new();
429 let missing = required
430 .iter()
431 .filter(|requirement| {
432 !results.iter().any(|result| {
433 normalized(&result.content).contains(&normalized(requirement))
434 && retrieval_queries
435 .iter()
436 .any(|q| normalized(q).contains(&normalized(requirement)))
437 })
438 })
439 .cloned()
440 .collect::<Vec<_>>();
441 if !missing.is_empty() {
442 gaps.push(EvidenceGapV1 {
443 schema_version: EVIDENCE_GAP_V1.into(),
444 gap_id: format!(
445 "gap:{}",
446 blake3::hash(missing.join("\n").as_bytes()).to_hex()
447 ),
448 reason: EvidenceGapReasonV1::MissingRequiredEvidence,
449 requirement: "required evidence terms".into(),
450 missing_items: missing.clone(),
451 diagnosed_by: vec!["content_and_route_coverage".into()],
452 follow_up_queries: missing.clone(),
453 });
454 }
455 if origin_denied {
456 gaps.push(EvidenceGapV1 {
457 schema_version: EVIDENCE_GAP_V1.into(),
458 gap_id: "gap:origin-authority".into(),
459 reason: EvidenceGapReasonV1::MissingOriginAuthority,
460 requirement: "origin-authorized recall".into(),
461 missing_items: vec!["origin authority denied or absent".into()],
462 diagnosed_by: vec!["governed_direct_id_or_search".into()],
463 follow_up_queries: Vec::new(),
464 });
465 }
466 if !results.is_empty()
467 && results
468 .iter()
469 .all(|result| state_status_for(result, resolution) != PremiseStatus::Supported)
470 {
471 gaps.push(EvidenceGapV1 {
472 schema_version: EVIDENCE_GAP_V1.into(),
473 gap_id: "gap:state-support".into(),
474 reason: EvidenceGapReasonV1::MissingStateSupport,
475 requirement: "current state support".into(),
476 missing_items: vec!["a supported current premise".into()],
477 diagnosed_by: vec!["state_dependency_resolution".into()],
478 follow_up_queries: vec![query.to_string()],
479 });
480 }
481 if results.is_empty() && !origin_denied && missing.is_empty() {
482 gaps.push(EvidenceGapV1 {
483 schema_version: EVIDENCE_GAP_V1.into(),
484 gap_id: "gap:no-candidates".into(),
485 reason: EvidenceGapReasonV1::NoCandidates,
486 requirement: "retrievable evidence".into(),
487 missing_items: vec![query.to_string()],
488 diagnosed_by: vec!["hybrid_search_empty".into()],
489 follow_up_queries: vec![query.to_string()],
490 });
491 }
492 gaps
493}
494
495fn resolution_status(
496 resolution: &DependencyResolutionV1,
497 results: &[SearchResult],
498) -> PremiseStatus {
499 if resolution.invalid_lineage || resolution.unresolved_conflict {
500 PremiseStatus::Ambiguous
501 } else if results.is_empty() {
502 PremiseStatus::Unsupported
503 } else if results
504 .iter()
505 .any(|result| state_status_for(result, resolution) == PremiseStatus::Supported)
506 {
507 PremiseStatus::Supported
508 } else if results
509 .iter()
510 .all(|result| state_status_for(result, resolution) == PremiseStatus::Stale)
511 {
512 PremiseStatus::Stale
513 } else {
514 PremiseStatus::Unsupported
515 }
516}
517
518fn packet_digest_input(items: &[EvidencePacketItemV1]) -> Vec<serde_json::Value> {
519 items
520 .iter()
521 .map(|item| {
522 serde_json::json!({
523 "id": item.result.source.result_id(),
524 "original_score": item.original_score,
525 "reranked_score": item.reranked_score,
526 "state_status": item.state_status,
527 "state_relevance": item.state_relevance,
528 "support_score": item.support_score,
529 "origin_authority_score": item.origin_authority_score,
530 })
531 })
532 .collect()
533}
534
535impl MemoryStore {
536 pub async fn retrieve_evidence(
539 &self,
540 request: EvidenceGapRequestV1,
541 ) -> Result<EvidencePacketV1, MemoryError> {
542 let state_view = request.state_mode.state_view();
543 let namespaces = request
544 .namespaces
545 .as_ref()
546 .map(|values| values.iter().map(String::as_str).collect::<Vec<_>>());
547 let namespace_refs = namespaces.as_deref();
548 let mut routes = Vec::new();
549 let mut consumed = 0usize;
550 let mut origin_denied = false;
551 let mut retrieval_queries = vec![request.query.clone()];
552 let mut results = Vec::new();
553
554 if request.budget > 0 {
555 consumed += 1;
556 let initial = if matches!(state_view, StateView::Current) {
557 let mut context = SearchContext::default_now();
558 context.request_id = Some(request.request_id.clone());
559 context.receipt_mode = crate::ReceiptMode::ReturnReceipt;
560 self.search_with_context(
561 &request.query,
562 Some(request.top_k),
563 namespace_refs,
564 None,
565 context,
566 )
567 .await?
568 .results
569 } else {
570 self.search_with_view(
571 &request.query,
572 Some(request.top_k),
573 namespace_refs,
574 None,
575 state_view.clone(),
576 )
577 .await?
578 };
579 let (admitted, denied) = self
580 .admit_results(initial, request.access_request.as_ref())
581 .await?;
582 origin_denied |= denied;
583 results.extend(admitted);
584 routes.push(EvidenceRouteReceiptV1 {
585 schema_version: EVIDENCE_ROUTE_RECEIPT_V1.into(),
586 route: EvidenceRetrievalRouteV1::InitialHybrid,
587 query: Some(request.query.clone()),
588 stage_outcome: StageOutcomeV1::Applied,
589 result_ids: ids(&results),
590 order_changed: false,
591 budget_unit: consumed,
592 note: None,
593 });
594 }
595
596 let (baseline_resolution, baseline_edges) = self
597 .resolve_evidence_state(&results, request.budget)
598 .await?;
599 let baseline_gaps = diagnose_gaps(
600 &results,
601 &request.required_evidence,
602 &retrieval_queries,
603 &baseline_resolution,
604 origin_denied,
605 &request.query,
606 );
607 let baseline_invalid = baseline_resolution.invalid_lineage;
608 let baseline_conflict = baseline_resolution.unresolved_conflict;
609 let baseline_sufficient = request.budget > 0
610 && !baseline_invalid
611 && !baseline_conflict
612 && evidence_sufficient(
613 &results,
614 &request.required_evidence,
615 &retrieval_queries,
616 &baseline_resolution,
617 );
618 let baseline_outcome = if request.budget == 0 {
619 EvidenceTerminalOutcomeV1::BudgetExceeded
620 } else if baseline_invalid {
621 EvidenceTerminalOutcomeV1::InvalidLineage
622 } else if baseline_conflict {
623 EvidenceTerminalOutcomeV1::Conflict
624 } else if baseline_sufficient {
625 EvidenceTerminalOutcomeV1::Sufficient
626 } else {
627 EvidenceTerminalOutcomeV1::EvidenceInsufficient
628 };
629 let baseline_items = self
630 .build_rerank_candidates(
631 &results,
632 &baseline_resolution,
633 request.access_request.as_ref(),
634 )
635 .await?;
636 let baseline_packet_digest = digest(&packet_digest_input(
637 &baseline_items
638 .iter()
639 .cloned()
640 .map(EvidencePacketItemV1::from)
641 .collect::<Vec<_>>(),
642 ))?;
643 let baseline_answer = baseline_items
644 .first()
645 .filter(|_| baseline_sufficient)
646 .map(|candidate| candidate.result.content.clone());
647
648 let mut gaps = baseline_gaps;
649 let mut final_outcome = baseline_outcome;
650 if request.budget > 0 && !baseline_invalid && !baseline_conflict && !baseline_sufficient {
651 let follow_limit = request.follow_up_limit.min(4);
652 let follow_queries = gaps
653 .iter()
654 .flat_map(|gap| gap.follow_up_queries.iter().cloned())
655 .filter(|query| !query.trim().is_empty())
656 .collect::<Vec<_>>();
657 for query in follow_queries.into_iter().take(follow_limit) {
658 if consumed >= request.budget {
659 break;
660 }
661 consumed += 1;
662 retrieval_queries.push(query.clone());
663 let before = ids(&results);
664 let found = if matches!(state_view, StateView::Current) {
665 self.search_fts_only(
666 &query,
667 Some(request.top_k),
668 namespace_refs,
669 Some(&[SearchSourceType::Facts, SearchSourceType::Chunks]),
670 )
671 .await?
672 } else {
673 self.search_with_view(
674 &query,
675 Some(request.top_k),
676 namespace_refs,
677 None,
678 state_view.clone(),
679 )
680 .await?
681 };
682 let (admitted, denied) = self
683 .admit_results(found, request.access_request.as_ref())
684 .await?;
685 origin_denied |= denied;
686 merge_results(&mut results, admitted);
687 let after = ids(&results);
688 routes.push(EvidenceRouteReceiptV1 {
689 schema_version: EVIDENCE_ROUTE_RECEIPT_V1.into(),
690 route: EvidenceRetrievalRouteV1::Lexical,
691 query: Some(query.clone()),
692 stage_outcome: StageOutcomeV1::Applied,
693 result_ids: after.clone(),
694 order_changed: before != after,
695 budget_unit: consumed,
696 note: Some("required evidence follow-up".into()),
697 });
698 let (resolution, _) = self
699 .resolve_evidence_state(&results, request.budget)
700 .await?;
701 if evidence_sufficient(
702 &results,
703 &request.required_evidence,
704 &retrieval_queries,
705 &resolution,
706 ) {
707 final_outcome = EvidenceTerminalOutcomeV1::Sufficient;
708 break;
709 }
710 }
711 }
712
713 if request.budget > consumed
716 && !matches!(
717 final_outcome,
718 EvidenceTerminalOutcomeV1::Sufficient
719 | EvidenceTerminalOutcomeV1::Conflict
720 | EvidenceTerminalOutcomeV1::InvalidLineage
721 )
722 && request.follow_up_limit >= 2
723 {
724 consumed += 1;
725 let before = ids(&results);
726 let found = if matches!(state_view, StateView::Current) {
727 let mut context = SearchContext::default_now();
728 context.request_id = Some(format!("{}:vector", request.request_id));
729 context.receipt_mode = crate::ReceiptMode::ReturnReceipt;
730 self.search_vector_only_with_context(
731 &request.query,
732 Some(request.top_k),
733 namespace_refs,
734 None,
735 context,
736 )
737 .await?
738 .results
739 } else {
740 Vec::new()
741 };
742 let (admitted, denied) = self
743 .admit_results(found, request.access_request.as_ref())
744 .await?;
745 origin_denied |= denied;
746 merge_results(&mut results, admitted);
747 let after = ids(&results);
748 routes.push(EvidenceRouteReceiptV1 {
749 schema_version: EVIDENCE_ROUTE_RECEIPT_V1.into(),
750 route: EvidenceRetrievalRouteV1::Vector,
751 query: Some(request.query.clone()),
752 stage_outcome: StageOutcomeV1::Applied,
753 result_ids: after.clone(),
754 order_changed: before != after,
755 budget_unit: consumed,
756 note: Some("bounded vector follow-up".into()),
757 });
758 }
759
760 if request.budget > consumed
762 && !matches!(
763 final_outcome,
764 EvidenceTerminalOutcomeV1::Conflict | EvidenceTerminalOutcomeV1::InvalidLineage
765 )
766 && request.follow_up_limit >= 3
767 {
768 consumed += 1;
769 let before = ids(&results);
770 let graph_results = self
771 .graph_follow_up(&results, request.access_request.as_ref(), request.top_k)
772 .await?;
773 let (admitted, denied) = self
774 .admit_results(graph_results, request.access_request.as_ref())
775 .await?;
776 origin_denied |= denied;
777 merge_results(&mut results, admitted);
778 let after = ids(&results);
779 routes.push(EvidenceRouteReceiptV1 {
780 schema_version: EVIDENCE_ROUTE_RECEIPT_V1.into(),
781 route: EvidenceRetrievalRouteV1::Graph,
782 query: None,
783 stage_outcome: StageOutcomeV1::Applied,
784 result_ids: after.clone(),
785 order_changed: before != after,
786 budget_unit: consumed,
787 note: Some("bounded state-neighborhood expansion".into()),
788 });
789 }
790
791 if request.budget > consumed
792 && !matches!(
793 final_outcome,
794 EvidenceTerminalOutcomeV1::Conflict | EvidenceTerminalOutcomeV1::InvalidLineage
795 )
796 && request.follow_up_limit >= 4
797 {
798 consumed += 1;
799 let before = ids(&results);
800 let found = if matches!(state_view, StateView::Current) {
801 self.search_with_view(
802 &request.query,
803 Some(request.top_k),
804 namespace_refs,
805 Some(&[SearchSourceType::Episodes]),
806 state_view.clone(),
807 )
808 .await?
809 } else {
810 self.search_with_view(
811 &request.query,
812 Some(request.top_k),
813 namespace_refs,
814 Some(&[SearchSourceType::Episodes]),
815 state_view.clone(),
816 )
817 .await?
818 };
819 let (admitted, denied) = self
820 .admit_results(found, request.access_request.as_ref())
821 .await?;
822 origin_denied |= denied;
823 merge_results(&mut results, admitted);
824 let after = ids(&results);
825 routes.push(EvidenceRouteReceiptV1 {
826 schema_version: EVIDENCE_ROUTE_RECEIPT_V1.into(),
827 route: EvidenceRetrievalRouteV1::Episodic,
828 query: Some(request.query.clone()),
829 stage_outcome: StageOutcomeV1::Applied,
830 result_ids: after.clone(),
831 order_changed: before != after,
832 budget_unit: consumed,
833 note: Some("bounded episodic follow-up".into()),
834 });
835 }
836
837 if !request.direct_ids.is_empty() {
838 let direct = self
839 .direct_id_results(&request.direct_ids, request.access_request.as_ref())
840 .await?;
841 if direct.1 {
842 origin_denied = true;
843 }
844 merge_results(&mut results, direct.0);
845 routes.push(EvidenceRouteReceiptV1 {
846 schema_version: EVIDENCE_ROUTE_RECEIPT_V1.into(),
847 route: EvidenceRetrievalRouteV1::DirectId,
848 query: None,
849 stage_outcome: if origin_denied {
850 StageOutcomeV1::Degraded
851 } else {
852 StageOutcomeV1::Applied
853 },
854 result_ids: ids(&results),
855 order_changed: false,
856 budget_unit: 0,
857 note: Some("direct IDs remain subject to governed origin admission".into()),
858 });
859 }
860
861 let (resolution, edges) = self
862 .resolve_evidence_state(&results, request.budget)
863 .await?;
864 let state_status = resolution_status(&resolution, &results);
865 let sufficient = request.budget > 0
866 && !resolution.invalid_lineage
867 && !resolution.unresolved_conflict
868 && evidence_sufficient(
869 &results,
870 &request.required_evidence,
871 &retrieval_queries,
872 &resolution,
873 );
874 final_outcome = if resolution.invalid_lineage {
875 EvidenceTerminalOutcomeV1::InvalidLineage
876 } else if resolution.unresolved_conflict {
877 EvidenceTerminalOutcomeV1::Conflict
878 } else if request.budget == 0
879 || (consumed >= request.budget && !sufficient && !results.is_empty())
880 {
881 EvidenceTerminalOutcomeV1::BudgetExceeded
882 } else if sufficient {
883 EvidenceTerminalOutcomeV1::Sufficient
884 } else {
885 EvidenceTerminalOutcomeV1::EvidenceInsufficient
886 };
887 gaps = diagnose_gaps(
888 &results,
889 &request.required_evidence,
890 &retrieval_queries,
891 &resolution,
892 origin_denied,
893 &request.query,
894 );
895
896 let mut candidates = self
897 .build_rerank_candidates(&results, &resolution, request.access_request.as_ref())
898 .await?;
899 candidates = rerank_state_aware(candidates, request.rerank_weights.clone());
900 candidates.truncate(request.top_k);
901 let items = candidates
902 .into_iter()
903 .map(EvidencePacketItemV1::from)
904 .collect::<Vec<_>>();
905 let final_packet_digest = digest(&packet_digest_input(&items))?;
906 let answer_policy = answer_policy_for(
907 state_status,
908 sufficient,
909 matches!(final_outcome, EvidenceTerminalOutcomeV1::Conflict),
910 matches!(final_outcome, EvidenceTerminalOutcomeV1::InvalidLineage),
911 matches!(final_outcome, EvidenceTerminalOutcomeV1::BudgetExceeded),
912 );
913 let answer = if matches!(final_outcome, EvidenceTerminalOutcomeV1::Sufficient) {
914 items.first().map(|item| item.result.content.clone())
915 } else {
916 None
917 };
918 let baseline_ids = baseline_items
919 .iter()
920 .map(|candidate| candidate.result.source.result_id())
921 .collect::<Vec<_>>();
922 let final_ids = items
923 .iter()
924 .map(|item| item.result.source.result_id())
925 .collect::<Vec<_>>();
926 let baseline_digest = baseline_packet_digest.clone();
927 let packet_changed = baseline_digest != final_packet_digest;
928 let order_changed = baseline_ids != final_ids;
929 let answer_changed = baseline_answer != answer;
930 let improved = matches!(final_outcome, EvidenceTerminalOutcomeV1::Sufficient)
931 && !matches!(baseline_outcome, EvidenceTerminalOutcomeV1::Sufficient)
932 && (packet_changed || answer_changed);
933 let ablation = EvidenceAblationReceiptV1 {
934 schema_version: EVIDENCE_PACKET_V1.into(),
935 baseline_result_ids: baseline_ids,
936 gap_loop_result_ids: final_ids,
937 packet_digest_without_gap_loop: baseline_digest,
938 packet_digest_with_gap_loop: final_packet_digest.clone(),
939 answer_without_gap_loop: baseline_answer,
940 answer_with_gap_loop: answer.clone(),
941 baseline_outcome,
942 final_outcome,
943 packet_changed,
944 order_changed,
945 answer_changed,
946 improved,
947 };
948
949 let resolved_ids = items
950 .iter()
951 .map(|item| item.result.source.result_id())
952 .collect::<Vec<_>>();
953 let alternative_ids = resolved_ids.iter().skip(1).cloned().collect::<Vec<_>>();
954 let mut state_receipt = StateResolutionReceiptV1::new(
955 request.request_id.clone(),
956 state_view.clone(),
957 request.state_mode.clone(),
958 state_status,
959 answer_policy.disposition,
960 resolved_ids,
961 alternative_ids,
962 sufficient,
963 matches!(final_outcome, EvidenceTerminalOutcomeV1::Conflict),
964 matches!(final_outcome, EvidenceTerminalOutcomeV1::InvalidLineage),
965 matches!(final_outcome, EvidenceTerminalOutcomeV1::BudgetExceeded),
966 )?;
967 state_receipt.stage_outcomes.push((
968 "evidence_gap".into(),
969 if matches!(final_outcome, EvidenceTerminalOutcomeV1::Sufficient) {
970 StageOutcomeV1::Applied
971 } else if matches!(final_outcome, EvidenceTerminalOutcomeV1::BudgetExceeded) {
972 StageOutcomeV1::BudgetExceeded
973 } else {
974 StageOutcomeV1::Degraded
975 },
976 ));
977 state_receipt = state_receipt.with_dependency_edge_digests(
978 edges
979 .iter()
980 .filter_map(edge_from_stored)
981 .map(|edge| edge.digest())
982 .collect(),
983 )?;
984 let witness = crate::state_epistemics::witnessed_retrieval(
985 self,
986 request.request_id.clone(),
987 &request.query,
988 &items
989 .iter()
990 .map(|item| item.result.clone())
991 .collect::<Vec<_>>(),
992 )
993 .await?;
994 let packet_id = format!("evidence-packet:{final_packet_digest}");
995 Ok(EvidencePacketV1 {
996 schema_version: EVIDENCE_PACKET_V1.into(),
997 packet_id,
998 request_id: request.request_id,
999 query: request.query,
1000 state_view,
1001 outcome: final_outcome,
1002 items,
1003 gaps,
1004 routes,
1005 answer,
1006 answer_policy,
1007 state_resolution_receipt: state_receipt,
1008 retrieval_witness: witness,
1009 ablation,
1010 packet_digest: final_packet_digest,
1011 })
1012 }
1013
1014 pub async fn retrieve_evidence_gap(
1015 &self,
1016 request: EvidenceGapRequestV1,
1017 ) -> Result<EvidencePacketV1, MemoryError> {
1018 self.retrieve_evidence(request).await
1019 }
1020
1021 async fn admit_results(
1022 &self,
1023 results: Vec<SearchResult>,
1024 access: Option<&GovernedAccessRequestV1>,
1025 ) -> Result<(Vec<SearchResult>, bool), MemoryError> {
1026 let Some(request) = access else {
1027 return Ok((Vec::new(), !results.is_empty()));
1031 };
1032 let mut admitted = Vec::new();
1033 let mut denied = false;
1034 for result in results {
1035 let allowed = match &result.source {
1036 SearchSource::Fact { fact_id, .. } => {
1037 self.authority()
1038 .get_fact_governed(fact_id, request.clone())
1039 .await?
1040 .decision
1041 .allowed
1042 }
1043 _ => false,
1044 };
1045 if allowed {
1046 admitted.push(result);
1047 } else {
1048 denied = true;
1049 }
1050 }
1051 Ok((admitted, denied))
1052 }
1053
1054 async fn direct_id_results(
1055 &self,
1056 direct_ids: &[String],
1057 access: Option<&GovernedAccessRequestV1>,
1058 ) -> Result<(Vec<SearchResult>, bool), MemoryError> {
1059 let mut results = Vec::new();
1060 let mut denied = false;
1061 for raw_id in direct_ids {
1062 let fact_id = raw_id.strip_prefix("fact:").unwrap_or(raw_id);
1063 let fact = if let Some(request) = access {
1064 let governed = self
1065 .authority()
1066 .get_fact_governed(fact_id, request.clone())
1067 .await?;
1068 if !governed.decision.allowed {
1069 denied = true;
1070 None
1071 } else {
1072 governed.fact
1073 }
1074 } else {
1075 denied = true;
1076 None
1077 };
1078 if let Some(fact) = fact {
1079 results.push(SearchResult {
1080 content: fact.content,
1081 source: SearchSource::Fact {
1082 fact_id: fact.id,
1083 namespace: fact.namespace,
1084 },
1085 score: 0.0,
1086 bm25_rank: None,
1087 vector_rank: None,
1088 cosine_similarity: None,
1089 });
1090 }
1091 }
1092 Ok((results, denied))
1093 }
1094
1095 async fn graph_follow_up(
1096 &self,
1097 results: &[SearchResult],
1098 access: Option<&GovernedAccessRequestV1>,
1099 top_k: usize,
1100 ) -> Result<Vec<SearchResult>, MemoryError> {
1101 let seeds = results
1102 .iter()
1103 .filter_map(|result| match &result.source {
1104 SearchSource::Fact { fact_id, .. } => Some(format!("fact:{fact_id}")),
1105 _ => None,
1106 })
1107 .collect::<Vec<_>>();
1108 if seeds.is_empty() {
1109 return Ok(Vec::new());
1110 }
1111 let edges = self
1112 .list_graph_edges_for_neighborhood(seeds, 1, top_k.clamp(1, 32))
1113 .await?;
1114 let mut ids = BTreeSet::new();
1115 for edge in edges {
1116 for endpoint in [&edge.source, &edge.target] {
1117 if endpoint.starts_with("fact:") {
1118 ids.insert(endpoint.trim_start_matches("fact:").to_string());
1119 }
1120 }
1121 }
1122 let (facts, _) = self
1123 .direct_id_results(
1124 &ids.into_iter()
1125 .map(|id| format!("fact:{id}"))
1126 .collect::<Vec<_>>(),
1127 access,
1128 )
1129 .await?;
1130 Ok(facts)
1131 }
1132
1133 async fn resolve_evidence_state(
1134 &self,
1135 results: &[SearchResult],
1136 budget: usize,
1137 ) -> Result<(DependencyResolutionV1, Vec<crate::StoredGraphEdge>), MemoryError> {
1138 let roots = results
1139 .iter()
1140 .filter_map(|result| match &result.source {
1141 SearchSource::Fact { fact_id, .. } => Some(format!("fact:{fact_id}")),
1142 _ => None,
1143 })
1144 .collect::<Vec<_>>();
1145 if roots.is_empty() {
1146 return Ok((
1147 resolve_dependency_states(&[], &[], budget.max(1)),
1148 Vec::new(),
1149 ));
1150 }
1151 let edges = self
1152 .list_graph_edges_for_neighborhood(roots.clone(), 2, budget.clamp(1, 128))
1153 .await?;
1154 let typed = edges
1155 .iter()
1156 .filter_map(edge_from_stored)
1157 .collect::<Vec<_>>();
1158 Ok((
1159 resolve_dependency_states(&typed, &roots, budget.max(1)),
1160 edges,
1161 ))
1162 }
1163
1164 async fn build_rerank_candidates(
1165 &self,
1166 results: &[SearchResult],
1167 resolution: &DependencyResolutionV1,
1168 access: Option<&GovernedAccessRequestV1>,
1169 ) -> Result<Vec<StateRerankCandidateV1>, MemoryError> {
1170 let mut candidates = Vec::new();
1171 for result in results {
1172 let status = state_status_for(result, resolution);
1173 let origin = match (&result.source, access) {
1174 (SearchSource::Fact { fact_id, .. }, _) => {
1175 self.authority().get_origin_authority(fact_id).await?
1176 }
1177 _ => None,
1178 };
1179 let candidate = StateRerankCandidateV1::from_result(
1180 result.clone(),
1181 status,
1182 state_relevance(status),
1183 )
1184 .with_origin(origin.as_ref());
1185 candidates.push(candidate);
1186 }
1187 Ok(candidates)
1188 }
1189}
1190
1191fn merge_results(results: &mut Vec<SearchResult>, additions: Vec<SearchResult>) {
1192 let mut seen = results
1193 .iter()
1194 .map(|result| result.source.result_id())
1195 .collect::<BTreeSet<_>>();
1196 for result in additions {
1197 if seen.insert(result.source.result_id()) {
1198 results.push(result);
1199 }
1200 }
1201}