use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::query::recall::ScoredMemory;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetrievalMode {
VectorOnly,
Bm25Only,
HybridRrf,
Graph,
HarnessAware {
harness: HarnessKind,
format: EnvelopeFormat,
},
DomainScoped,
Reconstruct,
}
impl RetrievalMode {
pub fn to_strategy_str(&self) -> &'static str {
match self {
Self::VectorOnly => "semantic",
Self::Bm25Only => "lexical",
Self::HybridRrf | Self::HarnessAware { .. } => "auto",
Self::Graph => "graph",
Self::DomainScoped => "domain_scoped",
Self::Reconstruct => "reconstruct",
}
}
pub fn envelope_adapter(&self) -> Option<Box<dyn HarnessEnvelope>> {
let Self::HarnessAware { harness, format } = self else {
return None;
};
Some(adapter_for(*harness, format.clone()))
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DomainScope {
pub org_id: Option<String>,
pub namespace: Option<String>,
pub doc_class: Option<String>,
pub tags: Option<Vec<String>>,
}
impl DomainScope {
pub fn is_empty(&self) -> bool {
self.org_id.is_none()
&& self.namespace.is_none()
&& self.doc_class.is_none()
&& self.tags.as_ref().map(|t| t.is_empty()).unwrap_or(true)
}
pub fn matches(&self, record: &crate::model::memory::MemoryRecord) -> bool {
if let Some(ref org) = self.org_id
&& record.org_id.as_deref() != Some(org.as_str())
{
return false;
}
if let Some(ref ns) = self.namespace {
let tag_hit = record.tags.iter().any(|t| t == ns);
let meta_hit = record
.metadata
.get("namespace")
.and_then(|v| v.as_str())
.map(|v| v == ns)
.unwrap_or(false);
if !tag_hit && !meta_hit {
return false;
}
}
if let Some(ref dc) = self.doc_class {
let meta_hit = record
.metadata
.get("doc_class")
.and_then(|v| v.as_str())
.map(|v| v == dc)
.unwrap_or(false);
if !meta_hit {
return false;
}
}
if let Some(ref tags) = self.tags
&& !tags.iter().all(|t| record.tags.contains(t))
{
return false;
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningAuthorship {
ModelAuthored,
UserProvided,
ToolVerified,
Injected,
Unverified,
}
impl ReasoningAuthorship {
pub fn as_str(&self) -> &'static str {
match self {
Self::ModelAuthored => "model_authored",
Self::UserProvided => "user_provided",
Self::ToolVerified => "tool_verified",
Self::Injected => "injected",
Self::Unverified => "unverified",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReasoningProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub written_at: Option<String>,
pub authorship: ReasoningAuthorship,
}
impl ReasoningProvenance {
pub const METADATA_KEY: &'static str = "reasoning_provenance";
pub fn model_authored(source: impl Into<String>) -> Self {
Self {
source: Some(source.into()),
written_at: None,
authorship: ReasoningAuthorship::ModelAuthored,
}
}
pub fn injected(source: impl Into<String>) -> Self {
Self {
source: Some(source.into()),
written_at: None,
authorship: ReasoningAuthorship::Injected,
}
}
pub fn from_metadata(metadata: &serde_json::Value) -> Self {
metadata
.get(Self::METADATA_KEY)
.and_then(|v| serde_json::from_value::<ReasoningProvenance>(v.clone()).ok())
.unwrap_or(Self {
source: None,
written_at: None,
authorship: ReasoningAuthorship::Unverified,
})
}
pub fn from_record(record: &crate::model::memory::MemoryRecord) -> Self {
Self::from_metadata(&record.metadata)
}
pub fn attach(&self, metadata: &mut serde_json::Value) {
if !metadata.is_object() {
*metadata = serde_json::json!({});
}
if let Ok(v) = serde_json::to_value(self) {
metadata[Self::METADATA_KEY] = v;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningTrustAction {
Quarantine,
DownWeight,
}
fn default_down_weight() -> f32 {
0.1
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReasoningTrustPolicy {
pub trusted: Vec<ReasoningAuthorship>,
pub action: ReasoningTrustAction,
#[serde(default = "default_down_weight")]
pub down_weight_factor: f32,
}
impl Default for ReasoningTrustPolicy {
fn default() -> Self {
Self {
trusted: vec![
ReasoningAuthorship::ModelAuthored,
ReasoningAuthorship::UserProvided,
ReasoningAuthorship::ToolVerified,
],
action: ReasoningTrustAction::Quarantine,
down_weight_factor: default_down_weight(),
}
}
}
impl ReasoningTrustPolicy {
pub fn quarantine_untrusted() -> Self {
Self::default()
}
pub fn down_weight_untrusted(factor: f32) -> Self {
Self {
action: ReasoningTrustAction::DownWeight,
down_weight_factor: factor,
..Self::default()
}
}
fn admits_metadata(&self, metadata: &serde_json::Value) -> bool {
self.trusted
.contains(&ReasoningProvenance::from_metadata(metadata).authorship)
}
pub fn admits_record(&self, record: &crate::model::memory::MemoryRecord) -> bool {
self.admits_metadata(&record.metadata)
}
pub fn excludes_record(&self, record: &crate::model::memory::MemoryRecord) -> bool {
matches!(self.action, ReasoningTrustAction::Quarantine) && !self.admits_record(record)
}
pub fn rerank(&self, hits: &mut Vec<ScoredMemory>) -> usize {
match self.action {
ReasoningTrustAction::Quarantine => {
let before = hits.len();
hits.retain(|h| self.admits_metadata(&h.metadata));
before - hits.len()
}
ReasoningTrustAction::DownWeight => {
let mut affected = 0;
for h in hits.iter_mut() {
if !self.admits_metadata(&h.metadata) {
h.score *= self.down_weight_factor;
affected += 1;
}
}
hits.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
affected
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HarnessKind {
ClaudeCode,
Codex,
GeminiCli,
Chronos,
Generic,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EnvelopeFormat {
Inline,
FileBased { path_root: PathBuf },
SideChannel,
}
pub trait HarnessEnvelope {
fn shape(&self, hits: &[ScoredMemory]) -> String;
}
fn adapter_for(kind: HarnessKind, format: EnvelopeFormat) -> Box<dyn HarnessEnvelope> {
match kind {
HarnessKind::ClaudeCode => Box::new(ClaudeCodeEnvelope {
inline: matches!(format, EnvelopeFormat::Inline),
}),
HarnessKind::Codex => Box::new(CodexEnvelope {
file_based: matches!(format, EnvelopeFormat::FileBased { .. }),
}),
HarnessKind::GeminiCli => Box::new(GeminiCliEnvelope),
HarnessKind::Chronos => Box::new(ChronosEnvelope),
HarnessKind::Generic => Box::new(GenericEnvelope),
}
}
#[derive(Debug, Clone, Copy)]
pub struct ClaudeCodeEnvelope {
pub inline: bool,
}
impl HarnessEnvelope for ClaudeCodeEnvelope {
fn shape(&self, hits: &[ScoredMemory]) -> String {
let mut out = String::new();
out.push_str("# mnemo.recall (Claude Code envelope)\n\n");
for (i, m) in hits.iter().enumerate() {
if self.inline {
out.push_str(&format!(
"## hit {} (recall://{} • score {:.3})\n```\n{}\n```\n\n",
i + 1,
m.id,
m.score,
m.content
));
} else {
let first_line = m.content.lines().next().unwrap_or("").trim();
out.push_str(&format!(
"- hit {} → `recall://{}` (score {:.3}): {}\n",
i + 1,
m.id,
m.score,
first_line
));
}
}
out
}
}
#[derive(Debug, Clone, Copy)]
pub struct CodexEnvelope {
pub file_based: bool,
}
impl HarnessEnvelope for CodexEnvelope {
fn shape(&self, hits: &[ScoredMemory]) -> String {
if self.file_based {
let pointers: Vec<String> = hits
.iter()
.map(|m| format!("{{\"id\":\"{}\",\"score\":{:.3}}}", m.id, m.score))
.collect();
format!(
"{{\"envelope\":\"codex_file_based\",\"hits\":[{}]}}",
pointers.join(",")
)
} else {
let blocks: Vec<String> = hits
.iter()
.map(|m| {
format!(
"{{\"id\":\"{}\",\"score\":{:.3},\"content\":{}}}",
m.id,
m.score,
serde_json::to_string(&m.content).unwrap_or_default()
)
})
.collect();
format!(
"{{\"envelope\":\"codex_inline\",\"hits\":[{}]}}",
blocks.join(",")
)
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GeminiCliEnvelope;
impl HarnessEnvelope for GeminiCliEnvelope {
fn shape(&self, hits: &[ScoredMemory]) -> String {
let mut out = String::new();
out.push_str("mnemo recall (Gemini CLI envelope)\n");
for (i, m) in hits.iter().enumerate() {
out.push_str(&format!(
"[{}] score={:.3} id={} — {}\n",
i + 1,
m.score,
m.id,
m.content
));
}
out
}
}
#[derive(Debug, Clone, Copy)]
pub struct ChronosEnvelope;
impl HarnessEnvelope for ChronosEnvelope {
fn shape(&self, hits: &[ScoredMemory]) -> String {
let mut out = String::new();
out.push_str("chronos recall envelope\n");
for m in hits {
let first_line = m.content.lines().next().unwrap_or("").trim();
out.push_str(&format!("t={:.3} id={} :: {}\n", m.score, m.id, first_line));
}
out
}
}
#[derive(Debug, Clone, Copy)]
pub struct GenericEnvelope;
impl HarnessEnvelope for GenericEnvelope {
fn shape(&self, hits: &[ScoredMemory]) -> String {
let mut out = String::new();
for m in hits {
let content_safe = m.content.replace(['\t', '\n', '\r'], " ");
out.push_str(&format!("{}\t{:.3}\t{}\n", m.id, m.score, content_safe));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::memory::{MemoryType, Scope};
use uuid::Uuid;
fn make_hit(content: &str, score: f32) -> ScoredMemory {
ScoredMemory {
id: Uuid::now_v7(),
content: content.to_string(),
agent_id: "test-agent".to_string(),
memory_type: MemoryType::Episodic,
scope: Scope::Private,
importance: 0.5,
tags: vec![],
metadata: serde_json::Value::Null,
score,
access_count: 0,
created_at: "2026-05-17T00:00:00Z".to_string(),
updated_at: "2026-05-17T00:00:00Z".to_string(),
score_breakdown: None,
}
}
fn hit_with(content: &str, score: f32, auth: ReasoningAuthorship) -> ScoredMemory {
let mut h = make_hit(content, score);
ReasoningProvenance {
source: Some("t".into()),
written_at: None,
authorship: auth,
}
.attach(&mut h.metadata);
h
}
fn rec_with(auth: ReasoningAuthorship) -> crate::model::memory::MemoryRecord {
let mut r = crate::model::memory::MemoryRecord::new("a".into(), "c".into());
ReasoningProvenance {
source: None,
written_at: None,
authorship: auth,
}
.attach(&mut r.metadata);
r
}
#[test]
fn reasoning_provenance_fails_closed_to_unverified() {
let r = crate::model::memory::MemoryRecord::new("a".into(), "c".into());
assert_eq!(
ReasoningProvenance::from_record(&r).authorship,
ReasoningAuthorship::Unverified
);
assert!(!ReasoningTrustPolicy::default().admits_record(&r));
}
#[test]
fn injected_reasoning_is_excluded_but_model_authored_is_admitted() {
let policy = ReasoningTrustPolicy::quarantine_untrusted();
let injected = rec_with(ReasoningAuthorship::Injected);
let authored = rec_with(ReasoningAuthorship::ModelAuthored);
assert!(policy.excludes_record(&injected));
assert!(!policy.admits_record(&injected));
assert!(!policy.excludes_record(&authored));
assert!(policy.admits_record(&authored));
assert_eq!(
ReasoningProvenance::from_record(&injected).authorship,
ReasoningAuthorship::Injected
);
}
#[test]
fn rerank_quarantine_drops_only_untrusted() {
let policy = ReasoningTrustPolicy::quarantine_untrusted();
let mut hits = vec![
hit_with("clean", 0.9, ReasoningAuthorship::ModelAuthored),
hit_with("forged", 0.8, ReasoningAuthorship::Injected),
hit_with("user", 0.7, ReasoningAuthorship::UserProvided),
hit_with("unknown", 0.6, ReasoningAuthorship::Unverified),
];
let dropped = policy.rerank(&mut hits);
assert_eq!(dropped, 2); assert_eq!(hits.len(), 2);
assert!(
hits.iter()
.all(|h| h.content == "clean" || h.content == "user")
);
}
#[test]
fn rerank_downweight_demotes_forged_below_clean() {
let policy = ReasoningTrustPolicy::down_weight_untrusted(0.1);
let mut hits = vec![
hit_with("forged", 0.9, ReasoningAuthorship::Injected),
hit_with("clean", 0.5, ReasoningAuthorship::ModelAuthored),
];
let affected = policy.rerank(&mut hits);
assert_eq!(affected, 1);
assert_eq!(hits[0].content, "clean");
assert_eq!(hits.len(), 2); }
#[test]
fn retrieval_mode_round_trip_strategy_string() {
assert_eq!(RetrievalMode::VectorOnly.to_strategy_str(), "semantic");
assert_eq!(RetrievalMode::Bm25Only.to_strategy_str(), "lexical");
assert_eq!(RetrievalMode::HybridRrf.to_strategy_str(), "auto");
assert_eq!(RetrievalMode::Graph.to_strategy_str(), "graph");
assert_eq!(
RetrievalMode::DomainScoped.to_strategy_str(),
"domain_scoped"
);
assert_eq!(RetrievalMode::Reconstruct.to_strategy_str(), "reconstruct");
let harness = RetrievalMode::HarnessAware {
harness: HarnessKind::ClaudeCode,
format: EnvelopeFormat::Inline,
};
assert_eq!(harness.to_strategy_str(), "auto");
}
fn rec(
org: Option<&str>,
tags: &[&str],
metadata: serde_json::Value,
) -> crate::model::memory::MemoryRecord {
use crate::model::memory::{ConsolidationState, SourceType};
crate::model::memory::MemoryRecord {
id: Uuid::now_v7(),
agent_id: "a".to_string(),
content: "c".to_string(),
memory_type: MemoryType::Episodic,
scope: Scope::Private,
importance: 0.5,
tags: tags.iter().map(|t| t.to_string()).collect(),
metadata,
embedding: None,
content_hash: vec![],
prev_hash: None,
source_type: SourceType::Agent,
source_id: None,
consolidation_state: ConsolidationState::Raw,
access_count: 0,
org_id: org.map(str::to_string),
thread_id: None,
created_at: "2026-06-13T00:00:00Z".to_string(),
updated_at: "2026-06-13T00:00:00Z".to_string(),
last_accessed_at: None,
expires_at: None,
deleted_at: None,
decay_rate: None,
created_by: None,
version: 1,
prev_version_id: None,
quarantined: false,
quarantine_reason: None,
decay_function: None,
}
}
#[test]
fn domain_scope_matches_logical_and() {
let empty = DomainScope::default();
assert!(empty.is_empty());
assert!(empty.matches(&rec(Some("alpha"), &[], serde_json::Value::Null)));
let by_org = DomainScope {
org_id: Some("alpha".to_string()),
..Default::default()
};
assert!(by_org.matches(&rec(Some("alpha"), &[], serde_json::Value::Null)));
assert!(!by_org.matches(&rec(Some("beta"), &[], serde_json::Value::Null)));
let by_ns = DomainScope {
namespace: Some("legal".to_string()),
..Default::default()
};
assert!(by_ns.matches(&rec(None, &["legal"], serde_json::Value::Null)));
assert!(by_ns.matches(&rec(None, &[], serde_json::json!({"namespace": "legal"}))));
assert!(!by_ns.matches(&rec(None, &["hr"], serde_json::json!({"namespace": "hr"}))));
let combo = DomainScope {
org_id: Some("alpha".to_string()),
doc_class: Some("contract".to_string()),
..Default::default()
};
assert!(combo.matches(&rec(
Some("alpha"),
&[],
serde_json::json!({"doc_class": "contract"})
)));
assert!(!combo.matches(&rec(
Some("beta"),
&[],
serde_json::json!({"doc_class": "contract"})
)));
assert!(!combo.matches(&rec(
Some("alpha"),
&[],
serde_json::json!({"doc_class": "memo"})
)));
}
#[test]
fn retrieval_mode_serde_round_trip() {
for mode in [
RetrievalMode::VectorOnly,
RetrievalMode::Bm25Only,
RetrievalMode::HybridRrf,
RetrievalMode::Graph,
RetrievalMode::DomainScoped,
RetrievalMode::Reconstruct,
RetrievalMode::HarnessAware {
harness: HarnessKind::ClaudeCode,
format: EnvelopeFormat::Inline,
},
RetrievalMode::HarnessAware {
harness: HarnessKind::Codex,
format: EnvelopeFormat::FileBased {
path_root: PathBuf::from("/tmp/codex"),
},
},
RetrievalMode::HarnessAware {
harness: HarnessKind::Generic,
format: EnvelopeFormat::SideChannel,
},
] {
let s = serde_json::to_string(&mode).unwrap();
let back: RetrievalMode = serde_json::from_str(&s).unwrap();
assert_eq!(mode, back, "round-trip failed for {mode:?} via {s}");
}
}
#[test]
fn harness_aware_returns_envelope_adapter() {
let mode = RetrievalMode::HarnessAware {
harness: HarnessKind::ClaudeCode,
format: EnvelopeFormat::Inline,
};
assert!(mode.envelope_adapter().is_some());
assert!(RetrievalMode::HybridRrf.envelope_adapter().is_none());
}
#[test]
fn five_adapters_produce_distinct_envelope_shapes() {
let hits = vec![
make_hit("first hit content line\nsecond line", 0.91),
make_hit("another hit", 0.42),
];
let cc = ClaudeCodeEnvelope { inline: true }.shape(&hits);
let codex = CodexEnvelope { file_based: true }.shape(&hits);
let gemini = GeminiCliEnvelope.shape(&hits);
let chronos = ChronosEnvelope.shape(&hits);
let generic = GenericEnvelope.shape(&hits);
let shapes = [&cc, &codex, &gemini, &chronos, &generic];
for (i, a) in shapes.iter().enumerate() {
for (j, b) in shapes.iter().enumerate() {
if i != j {
assert_ne!(
a, b,
"adapter shapes {} and {} collided (both produced:\n{a})",
i, j
);
}
}
}
}
#[test]
fn claude_code_envelope_inline_vs_non_inline_differ() {
let hits = vec![make_hit("hello world", 0.5)];
let inline = ClaudeCodeEnvelope { inline: true }.shape(&hits);
let non_inline = ClaudeCodeEnvelope { inline: false }.shape(&hits);
assert!(inline.contains("```"), "inline must contain fenced block");
assert!(
!non_inline.contains("```"),
"non-inline must not contain fenced block"
);
}
#[test]
fn generic_envelope_is_tsv_safe() {
let hits = vec![make_hit("has\ttab\nand newline", 0.5)];
let env = GenericEnvelope.shape(&hits);
assert_eq!(env.lines().count(), 1);
let parts: Vec<&str> = env.trim_end().split('\t').collect();
assert_eq!(
parts.len(),
3,
"TSV envelope must have id\\tscore\\tcontent"
);
}
}