pub fn decay_score(importance: f64, half_life: f64, elapsed: f64) -> f64 {
if half_life > 0.0 {
importance * f64::powf(2.0, -elapsed.max(0.0) / half_life)
} else {
0.0
}
}
pub fn recency_score(age: f64) -> f64 {
f64::exp(-age.max(0.0) / (7.0 * 86400.0))
}
pub fn valence_boost(valence: f64) -> f64 {
1.0 + 0.3 * valence.abs()
}
const NEGATIVE_QUERY_WORDS: &[&str] = &[
"sad",
"frustrated",
"angry",
"bad",
"worst",
"low",
"lows",
"difficult",
"hard",
"struggle",
"pain",
"stress",
"anxious",
"upset",
"failed",
"failure",
"problem",
"negative",
"stressing",
"worried",
"tough",
];
const POSITIVE_QUERY_WORDS: &[&str] = &[
"happy",
"joy",
"great",
"best",
"high",
"good",
"wonderful",
"excited",
"proud",
"success",
"achievement",
"positive",
"celebration",
"love",
];
fn sentiment_stem(word: &str) -> &str {
for suffix in &[
"iest", "ness", "ment", "ful", "est", "ing", "ous", "ive", "ity", "ed", "er", "ly", "al",
"es", "s",
] {
if word.len() > suffix.len() + 2 && word.ends_with(suffix) {
return &word[..word.len() - suffix.len()];
}
}
word
}
pub fn detect_query_sentiment(query_text: &str) -> f64 {
let lower = query_text.to_lowercase();
let tokens: Vec<&str> = lower
.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.collect();
let matches_list = |token: &str, words: &[&str]| -> bool {
let stemmed = sentiment_stem(token);
words.iter().any(|w| {
token == *w
|| stemmed == *w
|| (token.len() >= 4 && token.starts_with(w))
|| (stemmed.len() >= 4 && w.starts_with(stemmed))
})
};
let neg_count = tokens
.iter()
.filter(|t| matches_list(t, NEGATIVE_QUERY_WORDS))
.count();
let pos_count = tokens
.iter()
.filter(|t| matches_list(t, POSITIVE_QUERY_WORDS))
.count();
if neg_count > pos_count {
-1.0
} else if pos_count > neg_count {
1.0
} else {
0.0
}
}
pub fn query_valence_boost(memory_valence: f64, query_sentiment: f64) -> f64 {
let base = 1.0 + 0.3 * memory_valence.abs();
if query_sentiment == 0.0 {
return base;
}
let alignment = if memory_valence.abs() < 1e-10 {
0.0
} else {
query_sentiment * memory_valence.signum()
};
base * (1.0 + 0.2 * alignment)
}
pub fn composite_score_with_sentiment(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
valence: f64,
query_sentiment: f64,
) -> f64 {
let base_rel = W_SIM * similarity * freshness_mult(decay, recency, W_DECAY, W_RECENCY);
let gate = importance_gate(similarity);
let imp_mult = 1.0 + gate * ALPHA_IMP * importance.min(1.0);
base_rel * imp_mult * query_valence_boost(valence, query_sentiment)
}
pub const GRAPH_SCALE: f64 = 0.125;
#[inline]
pub fn graph_mult(graph_proximity: f64) -> f64 {
1.0 + GRAPH_SCALE * graph_proximity.clamp(0.0, 1.0)
}
pub fn graph_composite_score_with_sentiment(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
valence: f64,
graph_proximity: f64,
query_sentiment: f64,
) -> f64 {
composite_score_with_sentiment(
similarity,
decay,
recency,
importance,
valence,
query_sentiment,
) * graph_mult(graph_proximity)
}
pub const W_SIM: f64 = 0.50;
pub const W_DECAY: f64 = 0.20;
pub const W_RECENCY: f64 = 0.30;
pub const FRESHNESS_SCALE: f64 = 0.25;
#[inline]
pub fn freshness_mult(decay: f64, recency: f64, w_decay: f64, w_recency: f64) -> f64 {
1.0 + FRESHNESS_SCALE * (w_decay * decay + w_recency * recency)
}
pub const GATE_K: f64 = 12.0;
pub const GATE_TAU: f64 = 0.25;
pub const ALPHA_IMP: f64 = 0.80;
pub const GW_SIM: f64 = 0.35;
pub const GW_DECAY: f64 = 0.15;
pub const GW_RECENCY: f64 = 0.20;
pub const GW_GRAPH: f64 = 0.30;
pub const GW_ALPHA_IMP: f64 = 0.60;
use crate::types::ScoreContributions;
#[inline]
fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
#[inline]
pub fn importance_gate(similarity: f64) -> f64 {
sigmoid(GATE_K * (similarity - GATE_TAU))
}
pub fn composite_score(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
valence: f64,
) -> f64 {
let base_rel = W_SIM * similarity * freshness_mult(decay, recency, W_DECAY, W_RECENCY);
let gate = importance_gate(similarity);
let imp_mult = 1.0 + gate * ALPHA_IMP * importance.min(1.0);
base_rel * imp_mult * valence_boost(valence)
}
pub fn standard_contributions(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
) -> ScoreContributions {
let gate = importance_gate(similarity);
ScoreContributions {
similarity: W_SIM * similarity,
decay: FRESHNESS_SCALE * W_DECAY * decay,
recency: FRESHNESS_SCALE * W_RECENCY * recency,
importance: gate * ALPHA_IMP * importance.min(1.0),
graph_proximity: 0.0,
}
}
pub fn graph_composite_score(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
valence: f64,
graph_proximity: f64,
) -> f64 {
if graph_proximity > 0.0 {
let base_rel = (GW_SIM * similarity + GW_GRAPH * graph_proximity)
* freshness_mult(decay, recency, GW_DECAY, GW_RECENCY);
let gate = importance_gate(similarity);
let imp_mult = 1.0 + gate * GW_ALPHA_IMP * importance.min(1.0);
base_rel * imp_mult * valence_boost(valence)
} else {
composite_score(similarity, decay, recency, importance, valence)
}
}
pub fn graph_contributions(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
graph_proximity: f64,
) -> ScoreContributions {
if graph_proximity > 0.0 {
let gate = importance_gate(similarity);
ScoreContributions {
similarity: GW_SIM * similarity,
decay: FRESHNESS_SCALE * GW_DECAY * decay,
recency: FRESHNESS_SCALE * GW_RECENCY * recency,
importance: gate * GW_ALPHA_IMP * importance.min(1.0),
graph_proximity: GW_GRAPH * graph_proximity,
}
} else {
standard_contributions(similarity, decay, recency, importance)
}
}
pub fn eviction_score(decay: f64, recency: f64, access_count: u32) -> f64 {
let access_resist =
((access_count as f64).ln_1p() / (ACCESS_SATURATION as f64).ln_1p()).min(1.0);
0.6 * decay + 0.4 * recency + ACCESS_WEIGHT * access_resist
}
pub const ACCESS_SATURATION: u32 = 20;
pub const ACCESS_WEIGHT: f64 = 0.5;
pub fn build_why(similarity: f64, recency: f64, decay: f64, valence: f64) -> Vec<String> {
let mut why = Vec::new();
if similarity > 0.5 {
why.push(format!("semantically similar ({similarity:.2})"));
}
if recency > 0.5 {
why.push("recent".to_string());
}
if decay > 0.3 {
why.push(format!("important (decay={decay:.2})"));
}
if valence.abs() > 0.5 {
why.push(format!("emotionally weighted ({valence:.2})"));
}
if why.is_empty() {
why.push("matched query".to_string());
}
why
}
pub fn adaptive_composite_score(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
valence: f64,
query_sentiment: f64,
weights: &crate::types::LearnedWeights,
) -> f64 {
let base_rel = weights.w_sim
* similarity
* freshness_mult(decay, recency, weights.w_decay, weights.w_recency);
let gate = sigmoid(GATE_K * (similarity - weights.gate_tau));
let imp_mult = 1.0 + gate * weights.alpha_imp * importance.min(1.0);
base_rel * imp_mult * query_valence_boost(valence, query_sentiment)
}
pub fn adaptive_contributions(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
weights: &crate::types::LearnedWeights,
) -> ScoreContributions {
let gate = sigmoid(GATE_K * (similarity - weights.gate_tau));
ScoreContributions {
similarity: weights.w_sim * similarity,
decay: FRESHNESS_SCALE * weights.w_decay * decay,
recency: FRESHNESS_SCALE * weights.w_recency * recency,
importance: gate * weights.alpha_imp * importance.min(1.0),
graph_proximity: 0.0,
}
}
pub fn adaptive_graph_composite_score(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
valence: f64,
graph_proximity: f64,
query_sentiment: f64,
weights: &crate::types::LearnedWeights,
) -> f64 {
adaptive_composite_score(
similarity,
decay,
recency,
importance,
valence,
query_sentiment,
weights,
) * graph_mult(graph_proximity)
}
pub fn adaptive_graph_contributions(
similarity: f64,
decay: f64,
recency: f64,
importance: f64,
graph_proximity: f64,
weights: &crate::types::LearnedWeights,
) -> ScoreContributions {
let mut c = adaptive_contributions(similarity, decay, recency, importance, weights);
c.graph_proximity = GRAPH_SCALE * graph_proximity.clamp(0.0, 1.0);
c
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decay_score_fresh() {
let score = decay_score(0.8, 604800.0, 0.0);
assert!((score - 0.8).abs() < 1e-10);
}
#[test]
fn test_decay_score_one_half_life() {
let score = decay_score(1.0, 100.0, 100.0);
assert!((score - 0.5).abs() < 1e-10);
}
#[test]
fn test_decay_score_zero_half_life() {
let score = decay_score(0.8, 0.0, 100.0);
assert_eq!(score, 0.0);
}
#[test]
fn test_recency_score_fresh() {
let score = recency_score(0.0);
assert!((score - 1.0).abs() < 1e-10);
}
#[test]
fn test_recency_score_seven_days() {
let score = recency_score(7.0 * 86400.0);
assert!((score - f64::exp(-1.0)).abs() < 1e-10);
}
#[test]
fn test_valence_boost_zero() {
assert!((valence_boost(0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_valence_boost_positive() {
assert!((valence_boost(1.0) - 1.3).abs() < 1e-10);
}
#[test]
fn test_valence_boost_negative() {
assert!((valence_boost(-0.5) - 1.15).abs() < 1e-10);
}
#[test]
fn test_importance_gate_high_similarity() {
let gate = importance_gate(0.8);
assert!(gate > 0.99, "gate at sim=0.8 should be ~1.0, got {gate}");
}
#[test]
fn test_importance_gate_low_similarity() {
let gate = importance_gate(0.05);
assert!(gate < 0.12, "gate at sim=0.05 should be small, got {gate}");
}
#[test]
fn test_importance_gate_at_threshold() {
let gate = importance_gate(GATE_TAU);
assert!(
(gate - 0.5).abs() < 1e-10,
"gate at sim=τ should be 0.5, got {gate}"
);
}
#[test]
fn test_importance_gate_monotonic() {
let low = importance_gate(0.1);
let mid = importance_gate(0.3);
let high = importance_gate(0.7);
assert!(mid > low, "gate should increase with similarity");
assert!(high > mid, "gate should increase with similarity");
}
#[test]
fn test_high_imp_low_sim_loses_to_low_imp_high_sim() {
let irrelevant_important = composite_score(0.10, 0.5, 0.5, 1.0, 0.0);
let relevant_normal = composite_score(0.60, 0.5, 0.5, 0.3, 0.0);
assert!(relevant_normal > irrelevant_important,
"relevant_normal ({relevant_normal:.4}) should beat irrelevant_important ({irrelevant_important:.4})");
}
#[test]
fn test_high_imp_high_sim_beats_low_imp_high_sim() {
let important = composite_score(0.70, 0.5, 0.5, 0.9, 0.0);
let normal = composite_score(0.70, 0.5, 0.5, 0.3, 0.0);
assert!(
important > normal,
"when both relevant, higher importance should win: {important:.4} vs {normal:.4}"
);
}
#[test]
fn test_composite_score_basic() {
let score = composite_score(1.0, 1.0, 1.0, 1.0, 0.0);
let base = W_SIM * 1.0 * freshness_mult(1.0, 1.0, W_DECAY, W_RECENCY);
let gate = importance_gate(1.0);
let expected = base * (1.0 + gate * ALPHA_IMP);
assert!(
(score - expected).abs() < 1e-10,
"expected {expected}, got {score}"
);
}
#[test]
fn test_composite_score_with_valence() {
let score = composite_score(1.0, 1.0, 1.0, 1.0, 1.0);
let base = W_SIM * 1.0 * freshness_mult(1.0, 1.0, W_DECAY, W_RECENCY);
let gate = importance_gate(1.0);
let expected = base * (1.0 + gate * ALPHA_IMP) * 1.3;
assert!((score - expected).abs() < 1e-10);
}
#[test]
fn test_graph_composite_zero_proximity_matches_original() {
let original = composite_score(0.8, 0.6, 0.9, 0.7, 0.3);
let graph = graph_composite_score(0.8, 0.6, 0.9, 0.7, 0.3, 0.0);
assert!((original - graph).abs() < 1e-10);
}
#[test]
fn test_graph_composite_with_proximity() {
let score = graph_composite_score(1.0, 1.0, 1.0, 1.0, 0.0, 1.0);
let base = (GW_SIM + GW_GRAPH) * freshness_mult(1.0, 1.0, GW_DECAY, GW_RECENCY);
let gate = importance_gate(1.0);
let expected = base * (1.0 + gate * GW_ALPHA_IMP);
assert!(
(score - expected).abs() < 1e-10,
"expected {expected}, got {score}"
);
}
#[test]
fn the_recency_wall_is_torn_down() {
let old_relevant = composite_score(0.6, 0.0, 0.0, 0.5, 0.0);
let fresh_irrelevant = composite_score(0.3, 1.0, 1.0, 0.5, 0.0);
assert!(
old_relevant > fresh_irrelevant,
"an old relevant record must outrank fresh irrelevant noise: \
{old_relevant:.4} vs {fresh_irrelevant:.4}"
);
}
#[test]
fn test_composite_monotonic_in_similarity() {
let low = composite_score(0.3, 0.5, 0.5, 0.5, 0.0);
let high = composite_score(0.9, 0.5, 0.5, 0.5, 0.0);
assert!(high > low, "higher similarity should yield higher score");
}
#[test]
fn test_composite_monotonic_in_importance() {
let low = composite_score(0.5, 0.5, 0.5, 0.2, 0.0);
let high = composite_score(0.5, 0.5, 0.5, 0.9, 0.0);
assert!(
high > low,
"higher importance should yield higher score (when sim>τ)"
);
}
#[test]
fn test_composite_monotonic_in_recency() {
let low = composite_score(0.5, 0.5, 0.1, 0.5, 0.0);
let high = composite_score(0.5, 0.5, 0.9, 0.5, 0.0);
assert!(high > low, "higher recency should yield higher score");
}
#[test]
fn test_valence_symmetric() {
assert!((valence_boost(0.7) - valence_boost(-0.7)).abs() < 1e-10);
}
#[test]
fn test_valence_always_geq_1() {
for v in [-1.0, -0.5, 0.0, 0.5, 1.0] {
assert!(
valence_boost(v) >= 1.0,
"valence_boost({v}) = {} < 1.0",
valence_boost(v)
);
}
}
#[test]
fn test_composite_non_negative() {
for &sim in &[0.0, 0.5, 1.0] {
for &dec in &[0.0, 0.5, 1.0] {
for &rec in &[0.0, 0.5, 1.0] {
for &imp in &[0.0, 0.5, 1.0] {
for &val in &[-1.0, 0.0, 1.0] {
let s = composite_score(sim, dec, rec, imp, val);
assert!(
s >= 0.0,
"composite_score({sim},{dec},{rec},{imp},{val}) = {s} < 0"
);
}
}
}
}
}
}
#[test]
fn test_graph_composite_non_negative() {
for &prox in &[0.0, 0.25, 0.5, 1.0] {
let s = graph_composite_score(0.5, 0.5, 0.5, 0.5, 0.0, prox);
assert!(
s >= 0.0,
"graph_composite with prox={prox} should be non-negative"
);
}
}
#[test]
fn test_graph_proximity_increases_score() {
let without = graph_composite_score(0.3, 0.8, 0.7, 0.6, 0.0, 0.0);
let with = graph_composite_score(0.3, 0.8, 0.7, 0.6, 0.0, 0.8);
assert!(
with > without,
"graph proximity (0.8) should increase score: without={without}, with={with}"
);
}
#[test]
fn test_decay_monotonic_in_elapsed() {
let fresh = decay_score(0.8, 604800.0, 0.0);
let old = decay_score(0.8, 604800.0, 604800.0);
let ancient = decay_score(0.8, 604800.0, 604800.0 * 10.0);
assert!(fresh > old);
assert!(old > ancient);
}
#[test]
fn test_recency_monotonic_in_age() {
let fresh = recency_score(0.0);
let week = recency_score(7.0 * 86400.0);
let month = recency_score(30.0 * 86400.0);
assert!(fresh > week);
assert!(week > month);
}
#[test]
fn test_build_why_always_nonempty() {
let why = build_why(0.0, 0.0, 0.0, 0.0);
assert!(
!why.is_empty(),
"build_why should always produce at least one reason"
);
assert_eq!(why[0], "matched query");
}
#[test]
fn test_build_why_contains_similarity() {
let why = build_why(0.9, 0.1, 0.1, 0.0);
assert!(why.iter().any(|w| w.contains("semantically similar")));
}
#[test]
fn test_importance_capped_at_1() {
let capped = composite_score(0.5, 0.5, 0.5, 5.0, 0.0);
let at_one = composite_score(0.5, 0.5, 0.5, 1.0, 0.0);
assert!(
(capped - at_one).abs() < 1e-10,
"importance should be capped at 1.0"
);
}
#[test]
fn test_eviction_score() {
let score = eviction_score(1.0, 1.0, 0);
assert!((score - 1.0).abs() < 1e-10, "max inputs, no access => 1.0");
let score_zero = eviction_score(0.0, 0.0, 0);
assert!(
(score_zero - 0.0).abs() < 1e-10,
"zero inputs, no access => 0.0"
);
let low = eviction_score(0.1, 0.5, 0);
let high = eviction_score(0.9, 0.5, 0);
assert!(high > low, "higher decay => higher eviction score");
let cold = eviction_score(0.2, 0.2, 0);
let hot = eviction_score(0.2, 0.2, 25);
assert!(
hot > cold,
"frequent recall must resist eviction: {hot} > {cold}"
);
let at_sat = eviction_score(0.2, 0.2, ACCESS_SATURATION);
let beyond = eviction_score(0.2, 0.2, ACCESS_SATURATION * 100);
assert!(
(beyond - at_sat).abs() < 0.05,
"access resistance saturates"
);
}
#[test]
fn test_irrelevant_anchor_cannot_dominate() {
let anchor = composite_score(0.08, 0.8, 0.3, 0.9, 0.0);
let daily = composite_score(0.65, 0.6, 0.8, 0.2, 0.0);
assert!(
daily > anchor,
"relevant daily ({daily:.4}) should beat irrelevant anchor ({anchor:.4})"
);
}
#[test]
fn test_detect_query_sentiment_negative() {
assert_eq!(
detect_query_sentiment("What failures and problems have been stressing me out?"),
-1.0
);
assert_eq!(
detect_query_sentiment("Tell me about my emotional lows"),
-1.0
);
assert_eq!(
detect_query_sentiment("What was difficult this year?"),
-1.0
);
}
#[test]
fn test_detect_query_sentiment_positive() {
assert_eq!(detect_query_sentiment("What good things happened?"), 1.0);
assert_eq!(detect_query_sentiment("Tell me about happy moments"), 1.0);
assert_eq!(
detect_query_sentiment("What was my greatest achievement?"),
1.0
);
}
#[test]
fn test_detect_query_sentiment_neutral() {
assert_eq!(
detect_query_sentiment("What happened at work recently?"),
0.0
);
assert_eq!(detect_query_sentiment("Tell me about my family"), 0.0);
}
#[test]
fn test_query_valence_boost_neutral_query_matches_standard() {
for &v in &[-1.0, -0.5, 0.0, 0.5, 1.0] {
let standard = valence_boost(v);
let query_aware = query_valence_boost(v, 0.0);
assert!((standard - query_aware).abs() < 1e-10,
"neutral query should match standard: valence={v}, standard={standard}, query_aware={query_aware}");
}
}
#[test]
fn test_query_valence_boost_negative_alignment() {
let negative_aligned = query_valence_boost(-0.8, -1.0);
let standard = valence_boost(-0.8);
assert!(negative_aligned > standard,
"negative query + negative memory should boost more: aligned={negative_aligned:.4}, standard={standard:.4}");
}
#[test]
fn test_query_valence_boost_negative_misaligned() {
let misaligned = query_valence_boost(0.8, -1.0);
let standard = valence_boost(0.8);
assert!(misaligned < standard,
"negative query + positive memory should boost less: misaligned={misaligned:.4}, standard={standard:.4}");
}
#[test]
fn test_query_valence_boost_always_positive() {
for &v in &[-1.0, -0.5, 0.0, 0.5, 1.0] {
for &s in &[-1.0, 0.0, 1.0] {
let boost = query_valence_boost(v, s);
assert!(
boost >= 0.8,
"query_valence_boost({v}, {s}) = {boost} should be positive"
);
}
}
}
#[test]
fn test_composite_with_sentiment_matches_original_for_neutral() {
let original = composite_score(0.6, 0.5, 0.7, 0.8, 0.3);
let with_sent = composite_score_with_sentiment(0.6, 0.5, 0.7, 0.8, 0.3, 0.0);
assert!(
(original - with_sent).abs() < 1e-10,
"neutral sentiment should match original: {original} vs {with_sent}"
);
}
#[test]
fn test_gate_tau_regression_anchor_still_loses() {
let anchor = composite_score(0.08, 0.8, 0.3, 0.9, 0.0);
let daily = composite_score(0.65, 0.6, 0.8, 0.2, 0.0);
assert!(daily > anchor,
"with GATE_TAU=0.25, relevant daily ({daily:.4}) should still beat irrelevant anchor ({anchor:.4})");
let gate = importance_gate(0.08);
assert!(gate < 0.2, "gate at sim=0.08 should be small, got {gate}");
}
}
#[cfg(test)]
mod graph_wall_tests {
use super::*;
use crate::types::LearnedWeights;
#[test]
fn maximal_graph_edge_cannot_beat_a_relevant_record() {
let irrelevant = graph_composite_score_with_sentiment(0.0026, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0);
let relevant = graph_composite_score_with_sentiment(0.309, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0);
assert!(
relevant > irrelevant,
"graph wall rebuilt: irrelevant+connected {irrelevant:.4} beat relevant {relevant:.4}"
);
}
#[test]
fn graph_uplift_is_bounded_by_graph_scale() {
let s_low = 0.50;
let s_high = s_low * (1.0 + GRAPH_SCALE) * 1.001; let connected = graph_composite_score_with_sentiment(s_low, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
let plain = graph_composite_score_with_sentiment(s_high, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
assert!(
plain > connected,
"a gap wider than GRAPH_SCALE was reversed: {plain:.6} vs {connected:.6}"
);
}
#[test]
fn an_infinitesimal_edge_does_not_penalise() {
let no_edge = graph_composite_score_with_sentiment(0.4, 0.5, 0.5, 0.8, 0.0, 0.0, 0.0);
let tiny_edge = graph_composite_score_with_sentiment(0.4, 0.5, 0.5, 0.8, 0.0, 1e-9, 0.0);
assert!(
tiny_edge >= no_edge,
"a tiny edge reduced the score: {tiny_edge:.6} < {no_edge:.6}"
);
assert!((tiny_edge - no_edge).abs() < 1e-6, "discontinuity at zero");
}
#[test]
fn zero_similarity_scores_zero_even_when_connected() {
let s = graph_composite_score_with_sentiment(0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0);
assert_eq!(s, 0.0, "a connected but irrelevant record scored {s}");
}
#[test]
fn adaptive_path_obeys_the_same_wall() {
let w = LearnedWeights::default();
let irrelevant = adaptive_graph_composite_score(0.0026, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, &w);
let relevant = adaptive_graph_composite_score(0.309, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, &w);
assert!(
relevant > irrelevant,
"adaptive graph wall: {irrelevant:.4} beat {relevant:.4}"
);
let no_edge = adaptive_graph_composite_score(0.4, 0.5, 0.5, 0.8, 0.0, 0.0, 0.0, &w);
let tiny = adaptive_graph_composite_score(0.4, 0.5, 0.5, 0.8, 0.0, 1e-9, 0.0, &w);
assert!(
(tiny - no_edge).abs() < 1e-6,
"adaptive discontinuity at zero"
);
}
#[test]
fn the_old_additive_form_inverted_the_ranking() {
let old = |sim: f64, prox: f64| {
let base_rel =
(GW_SIM * sim + GW_GRAPH * prox) * freshness_mult(1.0, 1.0, GW_DECAY, GW_RECENCY);
let imp_mult = 1.0 + importance_gate(sim) * GW_ALPHA_IMP * 1.0;
base_rel * imp_mult
};
let old_irrelevant = old(0.0026, 1.0);
let old_relevant = old(0.309, 0.0);
assert!(
old_irrelevant > old_relevant,
"history not reproduced: {old_irrelevant:.4} vs {old_relevant:.4}"
);
let now_irrelevant =
graph_composite_score_with_sentiment(0.0026, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0);
let now_relevant =
graph_composite_score_with_sentiment(0.309, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0);
assert!(
now_relevant > now_irrelevant,
"the fix does not fix it: {now_relevant:.4} vs {now_irrelevant:.4}"
);
}
#[test]
fn graph_still_breaks_ties_among_near_equals() {
let connected = graph_composite_score_with_sentiment(0.40, 0.5, 0.5, 0.5, 0.0, 1.0, 0.0);
let alone = graph_composite_score_with_sentiment(0.40, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0);
assert!(
connected > alone,
"graph evidence became inert: {connected:.6} vs {alone:.6}"
);
}
}