use std::str::FromStr;
use uuid::Uuid;
use khive_storage::types::PageRequest;
use khive_storage::EntityFilter;
use crate::error::{RuntimeError, RuntimeResult};
use crate::operations::Resolved;
use crate::reference_ring::ReferenceRing;
use crate::runtime::{KhiveRuntime, NamespaceToken};
#[derive(Clone, Debug, PartialEq)]
pub struct ReferenceCandidate {
pub id: Uuid,
pub name: Option<String>,
pub score: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ReferenceResolution {
Resolved { id: Uuid, confidence: f64 },
Ambiguous { candidates: Vec<ReferenceCandidate> },
NotFound,
}
const RING_EXACT_CONFIDENCE: f64 = 0.95;
const RING_SUBSTRING_CONFIDENCE: f64 = 0.7;
const RING_AUTO_RESOLVE_CONFIDENCE: f64 = 0.7;
const EXACT_NAME_CONFIDENCE: f64 = 0.98;
const SEARCH_MARGIN_RATIO: f64 = 2.0;
const SEARCH_SCORE_FLOOR: f64 = 0.0;
const SEARCH_RESOLVED_CONFIDENCE: f64 = 0.6;
pub async fn resolve_reference(
runtime: &KhiveRuntime,
ring: &ReferenceRing,
token: &NamespaceToken,
nl_ref: &str,
limit: u32,
entity_kind: Option<&str>,
) -> RuntimeResult<ReferenceResolution> {
let trimmed = nl_ref.trim();
if trimmed.is_empty() {
return Ok(ReferenceResolution::NotFound);
}
if let Ok(uuid) = Uuid::from_str(trimmed) {
return match runtime.resolve_by_id(token, uuid).await? {
Some(Resolved::Entity(_)) => Ok(ReferenceResolution::Resolved {
id: uuid,
confidence: 1.0,
}),
Some(_) | None => Ok(ReferenceResolution::NotFound),
};
}
if is_hex_prefix(trimmed) {
return match runtime.resolve_prefix_unfiltered(trimmed).await {
Ok(Some(uuid)) => match runtime.resolve_by_id(token, uuid).await? {
Some(Resolved::Entity(_)) => Ok(ReferenceResolution::Resolved {
id: uuid,
confidence: 1.0,
}),
Some(_) | None => Ok(ReferenceResolution::NotFound),
},
Ok(None) => Ok(ReferenceResolution::NotFound),
Err(RuntimeError::AmbiguousPrefix { matches, .. }) => {
let mut entity_matches = Vec::with_capacity(matches.len());
for id in matches {
if matches!(
runtime.resolve_by_id(token, id).await?,
Some(Resolved::Entity(_))
) {
entity_matches.push(id);
}
}
match entity_matches.len() {
0 => Ok(ReferenceResolution::NotFound),
1 => Ok(ReferenceResolution::Resolved {
id: entity_matches[0],
confidence: 1.0,
}),
_ => Ok(ReferenceResolution::Ambiguous {
candidates: entity_matches
.into_iter()
.map(|id| ReferenceCandidate {
id,
name: None,
score: 1.0,
})
.collect(),
}),
}
}
Err(e) => Err(e),
};
}
let actor = token.actor();
let actor_key = format!("{}:{}", actor.kind, actor.id);
let ring_entries = ring.snapshot(token.namespace().as_str(), &actor_key);
let needle = trimmed.to_ascii_lowercase();
let exact: Vec<ReferenceCandidate> = ring_entries
.iter()
.filter(|e| {
e.name
.as_deref()
.is_some_and(|n| n.to_ascii_lowercase() == needle)
})
.map(|e| ReferenceCandidate {
id: e.id,
name: e.name.clone(),
score: RING_EXACT_CONFIDENCE,
})
.collect();
if let Some(resolution) = resolve_from_candidates(exact) {
return Ok(resolution);
}
let substring: Vec<ReferenceCandidate> = ring_entries
.iter()
.filter(|e| {
e.name.as_deref().is_some_and(|n| {
let n_lower = n.to_ascii_lowercase();
n_lower.contains(&needle) || needle.contains(&n_lower)
})
})
.map(|e| ReferenceCandidate {
id: e.id,
name: e.name.clone(),
score: RING_SUBSTRING_CONFIDENCE,
})
.collect();
if let Some(resolution) = resolve_from_candidates(substring) {
return Ok(resolution);
}
if let Some(resolution) = exact_name_match(runtime, token, trimmed, entity_kind).await? {
return Ok(resolution);
}
let hits = runtime
.hybrid_search(
token,
trimmed,
None,
limit.max(1),
entity_kind,
None,
&[],
None,
)
.await?;
let candidates: Vec<ReferenceCandidate> = hits
.into_iter()
.filter(|h| h.score.to_f64() > SEARCH_SCORE_FLOOR)
.map(|h| ReferenceCandidate {
id: h.entity_id,
name: h.title,
score: h.score.to_f64(),
})
.collect();
match candidates.len() {
0 => Ok(ReferenceResolution::NotFound),
1 => Ok(ReferenceResolution::Resolved {
id: candidates[0].id,
confidence: SEARCH_RESOLVED_CONFIDENCE,
}),
_ => {
let top_score = candidates[0].score;
let second_score = candidates[1].score;
let decisive =
second_score <= f64::EPSILON || top_score / second_score >= SEARCH_MARGIN_RATIO;
if decisive {
Ok(ReferenceResolution::Resolved {
id: candidates[0].id,
confidence: SEARCH_RESOLVED_CONFIDENCE,
})
} else {
Ok(ReferenceResolution::Ambiguous { candidates })
}
}
}
}
fn resolve_from_candidates(candidates: Vec<ReferenceCandidate>) -> Option<ReferenceResolution> {
match candidates.len() {
0 => None,
1 => {
let top = &candidates[0];
Some(if top.score >= RING_AUTO_RESOLVE_CONFIDENCE {
ReferenceResolution::Resolved {
id: top.id,
confidence: top.score,
}
} else {
ReferenceResolution::Ambiguous { candidates }
})
}
_ => Some(ReferenceResolution::Ambiguous { candidates }),
}
}
async fn exact_name_match(
runtime: &KhiveRuntime,
token: &NamespaceToken,
name: &str,
entity_kind: Option<&str>,
) -> RuntimeResult<Option<ReferenceResolution>> {
let filter = EntityFilter {
name_exact: Some(name.to_string()),
kinds: entity_kind.map(|k| vec![k.to_string()]).unwrap_or_default(),
..EntityFilter::default()
};
let page = runtime
.entities(token)?
.query_entities(
token.namespace().as_str(),
filter,
PageRequest {
offset: 0,
limit: 10,
},
)
.await
.map_err(RuntimeError::Storage)?;
let total = page.total.unwrap_or(page.items.len() as u64);
let exact: Vec<ReferenceCandidate> = page
.items
.into_iter()
.map(|e| ReferenceCandidate {
id: e.id,
name: Some(e.name),
score: EXACT_NAME_CONFIDENCE,
})
.collect();
Ok(match total {
0 => None,
1 => exact
.into_iter()
.next()
.map(|top| ReferenceResolution::Resolved {
id: top.id,
confidence: EXACT_NAME_CONFIDENCE,
}),
_ => Some(ReferenceResolution::Ambiguous { candidates: exact }),
})
}
fn is_hex_prefix(s: &str) -> bool {
s.len() >= 8 && s.chars().all(|c| c.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::NamespaceToken as TokenCtor;
use khive_gate::ActorRef;
use khive_types::namespace::Namespace;
fn actor_token(actor_id: &str) -> NamespaceToken {
TokenCtor::mint_authorized(Namespace::local(), ActorRef::new("agent", actor_id))
}
#[tokio::test]
async fn id_string_passthrough_resolves_full_uuid() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token = actor_token("resolver-test");
let ring = ReferenceRing::new();
let entity = rt
.create_entity(
&token,
"concept",
None,
"PassthroughTarget",
None,
None,
vec![],
)
.await
.expect("create entity");
let resolution = resolve_reference(&rt, &ring, &token, &entity.id.to_string(), 5, None)
.await
.expect("resolve_reference");
assert_eq!(
resolution,
ReferenceResolution::Resolved {
id: entity.id,
confidence: 1.0
}
);
}
#[tokio::test]
async fn id_string_passthrough_never_errors_on_a_miss() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token = actor_token("resolver-test");
let ring = ReferenceRing::new();
let missing = Uuid::new_v4();
let resolution = resolve_reference(&rt, &ring, &token, &missing.to_string(), 5, None)
.await
.expect("must not error, only report NotFound");
assert_eq!(resolution, ReferenceResolution::NotFound);
}
#[tokio::test]
async fn ring_exact_match_resolves_without_search() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token = actor_token("resolver-test");
let ring = ReferenceRing::new();
let actor = token.actor();
let actor_key = format!("{}:{}", actor.kind, actor.id);
let id = Uuid::new_v4();
ring.admit(
token.namespace().as_str(),
&actor_key,
id,
Some("the old record".to_string()),
);
let resolution = resolve_reference(&rt, &ring, &token, "the old record", 5, None)
.await
.expect("resolve_reference");
assert_eq!(
resolution,
ReferenceResolution::Resolved {
id,
confidence: RING_EXACT_CONFIDENCE
}
);
}
#[tokio::test]
async fn ring_ambiguous_on_multiple_exact_matches() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token = actor_token("resolver-test");
let ring = ReferenceRing::new();
let actor = token.actor();
let actor_key = format!("{}:{}", actor.kind, actor.id);
let id_a = Uuid::new_v4();
let id_b = Uuid::new_v4();
ring.admit(
token.namespace().as_str(),
&actor_key,
id_a,
Some("duplicate name".to_string()),
);
ring.admit(
token.namespace().as_str(),
&actor_key,
id_b,
Some("duplicate name".to_string()),
);
let resolution = resolve_reference(&rt, &ring, &token, "duplicate name", 5, None)
.await
.expect("resolve_reference");
match resolution {
ReferenceResolution::Ambiguous { candidates } => {
assert_eq!(candidates.len(), 2);
}
other => panic!("expected Ambiguous, got {other:?}"),
}
}
#[tokio::test]
async fn no_ring_entry_and_no_search_hit_is_not_found() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token = actor_token("resolver-test");
let ring = ReferenceRing::new();
let resolution =
resolve_reference(&rt, &ring, &token, "nothing matches this at all", 5, None)
.await
.expect("resolve_reference");
assert_eq!(resolution, ReferenceResolution::NotFound);
}
#[tokio::test]
async fn actor_isolation_blocks_cross_actor_ring_reads() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token_a = actor_token("actor-a");
let token_b = actor_token("actor-b");
let ring = ReferenceRing::new();
let actor_a = token_a.actor();
let actor_key_a = format!("{}:{}", actor_a.kind, actor_a.id);
let id = Uuid::new_v4();
ring.admit(
token_a.namespace().as_str(),
&actor_key_a,
id,
Some("shared-namespace-name".to_string()),
);
let resolution = resolve_reference(&rt, &ring, &token_b, "shared-namespace-name", 5, None)
.await
.expect("resolve_reference");
assert_eq!(resolution, ReferenceResolution::NotFound);
}
#[tokio::test]
async fn exact_name_stage_survives_many_newer_case_variant_decoys() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token = actor_token("resolver-test");
let ring = ReferenceRing::new();
let target = rt
.create_entity(&token, "concept", None, "RoLoRA", None, None, vec![])
.await
.expect("create target entity");
let decoy_cases = ["rolora", "ROLORA", "RoLoRa", "roLORA"];
for i in 0..120 {
rt.create_entity(
&token,
"concept",
None,
decoy_cases[i % decoy_cases.len()],
None,
None,
vec![],
)
.await
.expect("create decoy entity");
}
let resolution = resolve_reference(&rt, &ring, &token, "RoLoRA", 5, None)
.await
.expect("resolve_reference");
assert_eq!(
resolution,
ReferenceResolution::Resolved {
id: target.id,
confidence: EXACT_NAME_CONFIDENCE,
}
);
}
#[tokio::test]
async fn exact_name_ambiguous_decision_uses_storage_total_not_page_len() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let token = actor_token("resolver-test");
let ring = ReferenceRing::new();
for _ in 0..11 {
rt.create_entity(&token, "concept", None, "DupeExactName", None, None, vec![])
.await
.expect("create duplicate-named entity");
}
let resolution = resolve_reference(&rt, &ring, &token, "DupeExactName", 5, None)
.await
.expect("resolve_reference");
match resolution {
ReferenceResolution::Ambiguous { candidates } => {
assert_eq!(
candidates.len(),
10,
"candidate set is a bounded 10-row sample of the 11 storage matches, \
not the complete set"
);
}
other => panic!("expected Ambiguous driven by storage total (11), got {other:?}"),
}
}
}