#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReinstatementContext {
pub episode_ids: Vec<String>,
pub entity_ids: Vec<String>,
pub namespace: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ReinstatementScore {
pub score: f32,
pub matched_episode: bool,
pub matched_entity: bool,
pub matched_namespace: bool,
}
const EPISODE_WEIGHT: f32 = 0.4;
const ENTITY_WEIGHT: f32 = 0.4;
const NAMESPACE_WEIGHT: f32 = 0.2;
pub fn compute_reinstatement_score(
ctx: &ReinstatementContext,
candidate_episode_ids: &[String],
candidate_entity_ids: &[String],
candidate_namespace: Option<&str>,
) -> ReinstatementScore {
let matched_episode = !ctx.episode_ids.is_empty()
&& candidate_episode_ids
.iter()
.any(|id| ctx.episode_ids.contains(id));
let matched_entity = !ctx.entity_ids.is_empty()
&& candidate_entity_ids
.iter()
.any(|id| ctx.entity_ids.contains(id));
let matched_namespace = match (ctx.namespace.as_deref(), candidate_namespace) {
(Some(a), Some(b)) => a == b,
_ => false,
};
let raw = if matched_episode { EPISODE_WEIGHT } else { 0.0 }
+ if matched_entity { ENTITY_WEIGHT } else { 0.0 }
+ if matched_namespace {
NAMESPACE_WEIGHT
} else {
0.0
};
ReinstatementScore {
score: raw.clamp(0.0, 1.0),
matched_episode,
matched_entity,
matched_namespace,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &str) -> String {
v.to_string()
}
#[test]
fn reinstatement_no_match_is_zero() {
let ctx = ReinstatementContext {
episode_ids: vec![s("ep-1")],
entity_ids: vec![s("ent-1")],
namespace: Some(s("ns-a")),
};
let result = compute_reinstatement_score(&ctx, &[], &[], None);
assert_eq!(result.score, 0.0);
assert!(!result.matched_episode);
assert!(!result.matched_entity);
assert!(!result.matched_namespace);
}
#[test]
fn reinstatement_full_match_is_one() {
let ctx = ReinstatementContext {
episode_ids: vec![s("ep-1")],
entity_ids: vec![s("ent-1")],
namespace: Some(s("ns-a")),
};
let result = compute_reinstatement_score(&ctx, &[s("ep-1")], &[s("ent-1")], Some("ns-a"));
assert!((result.score - 1.0).abs() < f32::EPSILON);
assert!(result.matched_episode);
assert!(result.matched_entity);
assert!(result.matched_namespace);
}
#[test]
fn reinstatement_episode_only() {
let ctx = ReinstatementContext {
episode_ids: vec![s("ep-2")],
entity_ids: vec![],
namespace: None,
};
let result = compute_reinstatement_score(&ctx, &[s("ep-2")], &[], None);
assert!((result.score - 0.4).abs() < 1e-6);
assert!(result.matched_episode);
assert!(!result.matched_entity);
assert!(!result.matched_namespace);
}
#[test]
fn reinstatement_entity_only() {
let ctx = ReinstatementContext {
episode_ids: vec![],
entity_ids: vec![s("ent-x")],
namespace: None,
};
let result = compute_reinstatement_score(&ctx, &[], &[s("ent-x")], None);
assert!((result.score - 0.4).abs() < 1e-6);
assert!(!result.matched_episode);
assert!(result.matched_entity);
}
#[test]
fn reinstatement_namespace_only() {
let ctx = ReinstatementContext {
episode_ids: vec![],
entity_ids: vec![],
namespace: Some(s("ns-b")),
};
let result = compute_reinstatement_score(&ctx, &[], &[], Some("ns-b"));
assert!((result.score - 0.2).abs() < 1e-6);
assert!(result.matched_namespace);
}
#[test]
fn reinstatement_score_bounded() {
let ctx = ReinstatementContext {
episode_ids: vec![s("ep-1"), s("ep-2")],
entity_ids: vec![s("ent-1")],
namespace: Some(s("ns")),
};
let result =
compute_reinstatement_score(&ctx, &[s("ep-1"), s("ep-2")], &[s("ent-1")], Some("ns"));
assert!(result.score >= 0.0 && result.score <= 1.0);
}
}