1use std::str::FromStr;
30
31use uuid::Uuid;
32
33use khive_storage::types::PageRequest;
34use khive_storage::EntityFilter;
35
36use crate::error::{RuntimeError, RuntimeResult};
37use crate::operations::Resolved;
38use crate::reference_ring::ReferenceRing;
39use crate::runtime::{KhiveRuntime, NamespaceToken};
40
41#[derive(Clone, Debug, PartialEq)]
43pub struct ReferenceCandidate {
44 pub id: Uuid,
45 pub name: Option<String>,
46 pub score: f64,
47}
48
49#[derive(Clone, Debug, PartialEq)]
52pub enum ReferenceResolution {
53 Resolved { id: Uuid, confidence: f64 },
54 Ambiguous { candidates: Vec<ReferenceCandidate> },
55 NotFound,
56}
57
58const RING_EXACT_CONFIDENCE: f64 = 0.95;
60const RING_SUBSTRING_CONFIDENCE: f64 = 0.7;
62const RING_AUTO_RESOLVE_CONFIDENCE: f64 = 0.7;
69const EXACT_NAME_CONFIDENCE: f64 = 0.98;
76const SEARCH_MARGIN_RATIO: f64 = 2.0;
82const SEARCH_VECTOR_SIMILARITY_FLOOR: f64 = 0.3;
89const SEARCH_RESOLVED_CONFIDENCE: f64 = 0.6;
96const STAGE4_MIN_SEARCH_LIMIT: u32 = 20;
117
118pub async fn resolve_reference(
127 runtime: &KhiveRuntime,
128 ring: &ReferenceRing,
129 token: &NamespaceToken,
130 nl_ref: &str,
131 limit: u32,
132 entity_kind: Option<&str>,
133) -> RuntimeResult<ReferenceResolution> {
134 resolve_reference_with_entity_type(runtime, ring, token, nl_ref, limit, entity_kind, None).await
135}
136
137#[allow(clippy::too_many_arguments)]
141pub async fn resolve_reference_with_entity_type(
142 runtime: &KhiveRuntime,
143 ring: &ReferenceRing,
144 token: &NamespaceToken,
145 nl_ref: &str,
146 limit: u32,
147 entity_kind: Option<&str>,
148 entity_type: Option<&str>,
149) -> RuntimeResult<ReferenceResolution> {
150 let trimmed = nl_ref.trim();
151 if trimmed.is_empty() {
152 return Ok(ReferenceResolution::NotFound);
153 }
154
155 if let Ok(uuid) = Uuid::from_str(trimmed) {
163 return match runtime.resolve_by_id(token, uuid).await? {
164 Some(Resolved::Entity(_)) => Ok(ReferenceResolution::Resolved {
165 id: uuid,
166 confidence: 1.0,
167 }),
168 Some(_) | None => Ok(ReferenceResolution::NotFound),
169 };
170 }
171 if is_hex_prefix(trimmed) {
172 return match runtime.resolve_prefix_unfiltered(trimmed).await {
173 Ok(Some(uuid)) => match runtime.resolve_by_id(token, uuid).await? {
174 Some(Resolved::Entity(_)) => Ok(ReferenceResolution::Resolved {
175 id: uuid,
176 confidence: 1.0,
177 }),
178 Some(_) | None => Ok(ReferenceResolution::NotFound),
179 },
180 Ok(None) => Ok(ReferenceResolution::NotFound),
181 Err(RuntimeError::AmbiguousPrefix { matches, .. }) => {
182 let mut entity_matches = Vec::with_capacity(matches.len());
183 for id in matches {
184 if matches!(
185 runtime.resolve_by_id(token, id).await?,
186 Some(Resolved::Entity(_))
187 ) {
188 entity_matches.push(id);
189 }
190 }
191 match entity_matches.len() {
192 0 => Ok(ReferenceResolution::NotFound),
193 1 => Ok(ReferenceResolution::Resolved {
194 id: entity_matches[0],
195 confidence: 1.0,
196 }),
197 _ => Ok(ReferenceResolution::Ambiguous {
198 candidates: entity_matches
199 .into_iter()
200 .map(|id| ReferenceCandidate {
201 id,
202 name: None,
203 score: 1.0,
204 })
205 .collect(),
206 }),
207 }
208 }
209 Err(e) => Err(e),
210 };
211 }
212
213 let actor = token.actor();
215 let actor_key = format!("{}:{}", actor.kind, actor.id);
216 let ring_entries = ring.snapshot(token.namespace().as_str(), &actor_key);
217 let needle = trimmed.to_ascii_lowercase();
218
219 let exact: Vec<ReferenceCandidate> = ring_entries
220 .iter()
221 .filter(|e| {
222 e.name
223 .as_deref()
224 .is_some_and(|n| n.to_ascii_lowercase() == needle)
225 })
226 .map(|e| ReferenceCandidate {
227 id: e.id,
228 name: e.name.clone(),
229 score: RING_EXACT_CONFIDENCE,
230 })
231 .collect();
232 if let Some(resolution) = resolve_from_candidates(exact) {
233 return Ok(resolution);
234 }
235
236 let substring: Vec<ReferenceCandidate> = ring_entries
237 .iter()
238 .filter(|e| {
239 e.name.as_deref().is_some_and(|n| {
240 let n_lower = n.to_ascii_lowercase();
241 n_lower.contains(&needle) || needle.contains(&n_lower)
242 })
243 })
244 .map(|e| ReferenceCandidate {
245 id: e.id,
246 name: e.name.clone(),
247 score: RING_SUBSTRING_CONFIDENCE,
248 })
249 .collect();
250 if let Some(resolution) = resolve_from_candidates(substring) {
251 return Ok(resolution);
252 }
253
254 if let Some(resolution) =
262 exact_name_match(runtime, token, trimmed, entity_kind, entity_type).await?
263 {
264 return Ok(resolution);
265 }
266
267 let candidate_limit = limit.max(1);
273 let search_limit = candidate_limit.max(STAGE4_MIN_SEARCH_LIMIT);
274 let hits = runtime
275 .hybrid_search_with_vector_similarity_floor(
276 token,
277 trimmed,
278 None,
279 search_limit,
280 entity_kind,
281 entity_type,
282 &[],
283 None,
284 SEARCH_VECTOR_SIMILARITY_FLOOR,
285 )
286 .await?;
287 let candidates: Vec<ReferenceCandidate> = hits
288 .into_iter()
289 .map(|h| ReferenceCandidate {
290 id: h.entity_id,
291 name: h.title,
292 score: h.score.to_f64(),
293 })
294 .collect();
295
296 match candidates.len() {
297 0 => Ok(ReferenceResolution::NotFound),
298 1 => Ok(ReferenceResolution::Resolved {
302 id: candidates[0].id,
303 confidence: SEARCH_RESOLVED_CONFIDENCE,
304 }),
305 _ => {
306 let top_score = candidates[0].score;
307 let second_score = candidates[1].score;
308 let decisive =
309 second_score <= f64::EPSILON || top_score / second_score >= SEARCH_MARGIN_RATIO;
310 if decisive {
311 Ok(ReferenceResolution::Resolved {
312 id: candidates[0].id,
313 confidence: SEARCH_RESOLVED_CONFIDENCE,
314 })
315 } else {
316 let mut candidates = candidates;
323 candidates.truncate(candidate_limit as usize);
324 Ok(ReferenceResolution::Ambiguous { candidates })
325 }
326 }
327 }
328}
329
330fn resolve_from_candidates(candidates: Vec<ReferenceCandidate>) -> Option<ReferenceResolution> {
336 match candidates.len() {
337 0 => None,
338 1 => {
339 let top = &candidates[0];
340 Some(if top.score >= RING_AUTO_RESOLVE_CONFIDENCE {
341 ReferenceResolution::Resolved {
342 id: top.id,
343 confidence: top.score,
344 }
345 } else {
346 ReferenceResolution::Ambiguous { candidates }
347 })
348 }
349 _ => Some(ReferenceResolution::Ambiguous { candidates }),
350 }
351}
352
353async fn exact_name_match(
364 runtime: &KhiveRuntime,
365 token: &NamespaceToken,
366 name: &str,
367 entity_kind: Option<&str>,
368 entity_type: Option<&str>,
369) -> RuntimeResult<Option<ReferenceResolution>> {
370 let mut filter = EntityFilter {
371 name_exact: Some(name.to_string()),
372 kinds: entity_kind.map(|k| vec![k.to_string()]).unwrap_or_default(),
373 ..EntityFilter::default()
374 };
375 if let (Some(kind), Some(entity_type)) = (entity_kind, entity_type) {
376 filter
377 .entity_types_by_kind
378 .insert(kind.to_string(), vec![entity_type.to_string()]);
379 }
380 let page = runtime
393 .entities(token)?
394 .query_entities(
395 token.namespace().as_str(),
396 filter,
397 PageRequest {
398 offset: 0,
399 limit: 10,
400 },
401 )
402 .await
403 .map_err(RuntimeError::Storage)?;
404
405 let total = page.total.unwrap_or(page.items.len() as u64);
406
407 let exact: Vec<ReferenceCandidate> = page
408 .items
409 .into_iter()
410 .map(|e| ReferenceCandidate {
411 id: e.id,
412 name: Some(e.name),
413 score: EXACT_NAME_CONFIDENCE,
414 })
415 .collect();
416
417 Ok(match total {
418 0 => None,
419 1 => exact
420 .into_iter()
421 .next()
422 .map(|top| ReferenceResolution::Resolved {
423 id: top.id,
424 confidence: EXACT_NAME_CONFIDENCE,
425 }),
426 _ => Some(ReferenceResolution::Ambiguous { candidates: exact }),
427 })
428}
429
430fn is_hex_prefix(s: &str) -> bool {
431 s.len() >= 8 && s.chars().all(|c| c.is_ascii_hexdigit())
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::config::{NamespaceToken as TokenCtor, RuntimeConfig};
438 use crate::embedder_registry::EmbedderProvider;
439 use crate::retrieval::SearchSource;
440 use khive_gate::ActorRef;
441 use khive_types::{namespace::Namespace, SubstrateKind};
442 use lattice_embed::{EmbeddingModel, EmbeddingService};
443 use std::sync::Arc;
444
445 struct ConstantEmbeddingService {
446 dimensions: usize,
447 }
448
449 #[async_trait::async_trait]
450 impl EmbeddingService for ConstantEmbeddingService {
451 async fn embed(
452 &self,
453 texts: &[String],
454 _model: EmbeddingModel,
455 ) -> Result<Vec<Vec<f32>>, lattice_embed::EmbedError> {
456 Ok(texts.iter().map(|_| vec![1.0; self.dimensions]).collect())
457 }
458
459 fn supports_model(&self, _model: EmbeddingModel) -> bool {
460 true
461 }
462
463 fn name(&self) -> &'static str {
464 "resolve-test-constant-embedding"
465 }
466 }
467
468 struct ConstantEmbedderProvider {
469 name: String,
470 dimensions: usize,
471 }
472
473 #[async_trait::async_trait]
474 impl EmbedderProvider for ConstantEmbedderProvider {
475 fn name(&self) -> &str {
476 &self.name
477 }
478
479 fn dimensions(&self) -> usize {
480 self.dimensions
481 }
482
483 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
484 Ok(Arc::new(ConstantEmbeddingService {
485 dimensions: self.dimensions,
486 }))
487 }
488 }
489
490 fn runtime_with_constant_embeddings() -> KhiveRuntime {
491 let model = EmbeddingModel::AllMiniLmL6V2;
492 let runtime = KhiveRuntime::new(RuntimeConfig {
493 db_path: None,
494 embedding_model: Some(model),
495 packs: vec!["kg".to_string()],
496 ..RuntimeConfig::no_embeddings()
497 })
498 .expect("in-memory runtime");
499 runtime.register_embedder(ConstantEmbedderProvider {
500 name: model.to_string(),
501 dimensions: model.dimensions(),
502 });
503 runtime
504 }
505
506 fn actor_token(actor_id: &str) -> NamespaceToken {
507 TokenCtor::mint_authorized(Namespace::local(), ActorRef::new("agent", actor_id))
508 }
509
510 #[tokio::test]
511 async fn id_string_passthrough_resolves_full_uuid() {
512 let rt = KhiveRuntime::memory().expect("in-memory runtime");
513 let token = actor_token("resolver-test");
514 let ring = ReferenceRing::new();
515
516 let entity = rt
517 .create_entity(
518 &token,
519 "concept",
520 None,
521 "PassthroughTarget",
522 None,
523 None,
524 vec![],
525 )
526 .await
527 .expect("create entity");
528
529 let resolution = resolve_reference(&rt, &ring, &token, &entity.id.to_string(), 5, None)
530 .await
531 .expect("resolve_reference");
532 assert_eq!(
533 resolution,
534 ReferenceResolution::Resolved {
535 id: entity.id,
536 confidence: 1.0
537 }
538 );
539 }
540
541 #[tokio::test]
542 async fn id_string_passthrough_never_errors_on_a_miss() {
543 let rt = KhiveRuntime::memory().expect("in-memory runtime");
544 let token = actor_token("resolver-test");
545 let ring = ReferenceRing::new();
546
547 let missing = Uuid::new_v4();
548 let resolution = resolve_reference(&rt, &ring, &token, &missing.to_string(), 5, None)
549 .await
550 .expect("must not error, only report NotFound");
551 assert_eq!(resolution, ReferenceResolution::NotFound);
552 }
553
554 #[tokio::test]
555 async fn ring_exact_match_resolves_without_search() {
556 let rt = KhiveRuntime::memory().expect("in-memory runtime");
557 let token = actor_token("resolver-test");
558 let ring = ReferenceRing::new();
559 let actor = token.actor();
560 let actor_key = format!("{}:{}", actor.kind, actor.id);
561
562 let id = Uuid::new_v4();
563 ring.admit(
564 token.namespace().as_str(),
565 &actor_key,
566 id,
567 Some("the old record".to_string()),
568 );
569
570 let resolution = resolve_reference(&rt, &ring, &token, "the old record", 5, None)
571 .await
572 .expect("resolve_reference");
573 assert_eq!(
574 resolution,
575 ReferenceResolution::Resolved {
576 id,
577 confidence: RING_EXACT_CONFIDENCE
578 }
579 );
580 }
581
582 #[tokio::test]
583 async fn ring_ambiguous_on_multiple_exact_matches() {
584 let rt = KhiveRuntime::memory().expect("in-memory runtime");
585 let token = actor_token("resolver-test");
586 let ring = ReferenceRing::new();
587 let actor = token.actor();
588 let actor_key = format!("{}:{}", actor.kind, actor.id);
589
590 let id_a = Uuid::new_v4();
591 let id_b = Uuid::new_v4();
592 ring.admit(
593 token.namespace().as_str(),
594 &actor_key,
595 id_a,
596 Some("duplicate name".to_string()),
597 );
598 ring.admit(
599 token.namespace().as_str(),
600 &actor_key,
601 id_b,
602 Some("duplicate name".to_string()),
603 );
604
605 let resolution = resolve_reference(&rt, &ring, &token, "duplicate name", 5, None)
606 .await
607 .expect("resolve_reference");
608 match resolution {
609 ReferenceResolution::Ambiguous { candidates } => {
610 assert_eq!(candidates.len(), 2);
611 }
612 other => panic!("expected Ambiguous, got {other:?}"),
613 }
614 }
615
616 #[tokio::test]
617 async fn no_ring_entry_and_no_search_hit_is_not_found() {
618 let rt = KhiveRuntime::memory().expect("in-memory runtime");
619 let token = actor_token("resolver-test");
620 let ring = ReferenceRing::new();
621
622 let resolution =
623 resolve_reference(&rt, &ring, &token, "nothing matches this at all", 5, None)
624 .await
625 .expect("resolve_reference");
626 assert_eq!(resolution, ReferenceResolution::NotFound);
627 }
628
629 #[tokio::test]
630 async fn actor_isolation_blocks_cross_actor_ring_reads() {
631 let rt = KhiveRuntime::memory().expect("in-memory runtime");
632 let token_a = actor_token("actor-a");
633 let token_b = actor_token("actor-b");
634 let ring = ReferenceRing::new();
635 let actor_a = token_a.actor();
636 let actor_key_a = format!("{}:{}", actor_a.kind, actor_a.id);
637
638 let id = Uuid::new_v4();
639 ring.admit(
640 token_a.namespace().as_str(),
641 &actor_key_a,
642 id,
643 Some("shared-namespace-name".to_string()),
644 );
645
646 let resolution = resolve_reference(&rt, &ring, &token_b, "shared-namespace-name", 5, None)
648 .await
649 .expect("resolve_reference");
650 assert_eq!(resolution, ReferenceResolution::NotFound);
651 }
652
653 #[tokio::test]
664 async fn exact_name_stage_survives_many_newer_case_variant_decoys() {
665 let rt = KhiveRuntime::memory().expect("in-memory runtime");
666 let token = actor_token("resolver-test");
667 let ring = ReferenceRing::new();
668
669 let target = rt
670 .create_entity(&token, "concept", None, "RoLoRA", None, None, vec![])
671 .await
672 .expect("create target entity");
673
674 let decoy_cases = ["rolora", "ROLORA", "RoLoRa", "roLORA"];
681 for i in 0..120 {
682 rt.create_entity(
683 &token,
684 "concept",
685 None,
686 decoy_cases[i % decoy_cases.len()],
687 None,
688 None,
689 vec![],
690 )
691 .await
692 .expect("create decoy entity");
693 }
694
695 let resolution = resolve_reference(&rt, &ring, &token, "RoLoRA", 5, None)
696 .await
697 .expect("resolve_reference");
698 assert_eq!(
699 resolution,
700 ReferenceResolution::Resolved {
701 id: target.id,
702 confidence: EXACT_NAME_CONFIDENCE,
703 }
704 );
705 }
706
707 #[tokio::test]
718 async fn exact_name_ambiguous_decision_uses_storage_total_not_page_len() {
719 let rt = KhiveRuntime::memory().expect("in-memory runtime");
720 let token = actor_token("resolver-test");
721 let ring = ReferenceRing::new();
722
723 for _ in 0..11 {
724 rt.create_entity(&token, "concept", None, "DupeExactName", None, None, vec![])
725 .await
726 .expect("create duplicate-named entity");
727 }
728
729 let resolution = resolve_reference(&rt, &ring, &token, "DupeExactName", 5, None)
730 .await
731 .expect("resolve_reference");
732
733 match resolution {
734 ReferenceResolution::Ambiguous { candidates } => {
735 assert_eq!(
736 candidates.len(),
737 10,
738 "candidate set is a bounded 10-row sample of the 11 storage matches, \
739 not the complete set"
740 );
741 }
742 other => panic!("expected Ambiguous driven by storage total (11), got {other:?}"),
743 }
744 }
745
746 #[tokio::test]
756 async fn exact_name_resolves_ahead_of_competing_hybrid_matches() {
757 let rt = KhiveRuntime::memory().expect("in-memory runtime");
758 let token = actor_token("resolver-test");
759 let ring = ReferenceRing::new();
760
761 let target = rt
762 .create_entity(
763 &token,
764 "concept",
765 None,
766 "ADR-040",
767 Some("khive ADR-040"),
768 None,
769 vec![],
770 )
771 .await
772 .expect("create target entity");
773
774 for i in 0..12 {
778 rt.create_entity(
779 &token,
780 "concept",
781 None,
782 &format!("ADR-040 companion {i}"),
783 Some("ADR-040 ADR-040 ADR-040 ADR-040 ADR-040"),
784 None,
785 vec![],
786 )
787 .await
788 .expect("create competing entity");
789 }
790
791 let resolution = resolve_reference(&rt, &ring, &token, "ADR-040", 1, None)
794 .await
795 .expect("resolve_reference");
796 assert_eq!(
797 resolution,
798 ReferenceResolution::Resolved {
799 id: target.id,
800 confidence: EXACT_NAME_CONFIDENCE,
801 }
802 );
803 }
804
805 #[tokio::test]
813 async fn fallback_stage_bounds_non_exact_payload_to_limit() {
814 let rt = KhiveRuntime::memory().expect("in-memory runtime");
815 let token = actor_token("resolver-test");
816 let ring = ReferenceRing::new();
817
818 for i in 0..12 {
822 rt.create_entity(
823 &token,
824 "concept",
825 None,
826 &format!("Retrieval Fusion Note {i}"),
827 Some("khive retrieval fusion ranking note"),
828 None,
829 vec![],
830 )
831 .await
832 .expect("create entity");
833 }
834
835 let resolution = resolve_reference(
836 &rt,
837 &ring,
838 &token,
839 "khive retrieval fusion ranking",
840 5,
841 None,
842 )
843 .await
844 .expect("resolve_reference");
845
846 match resolution {
847 ReferenceResolution::Ambiguous { candidates } => {
848 assert_eq!(
849 candidates.len(),
850 5,
851 "a non-exact ref's ambiguity payload is bounded to the caller's limit"
852 );
853 }
854 other => panic!("expected a bounded Ambiguous sample, got {other:?}"),
855 }
856 }
857
858 #[tokio::test]
862 async fn fallback_stage_still_reports_ambiguous_on_genuine_tie() {
863 let rt = KhiveRuntime::memory().expect("in-memory runtime");
864 let token = actor_token("resolver-test");
865 let ring = ReferenceRing::new();
866
867 let a = rt
868 .create_entity(
869 &token,
870 "concept",
871 None,
872 "Twin Record",
873 Some("khive Twin Record document"),
874 None,
875 vec![],
876 )
877 .await
878 .expect("create entity a");
879 let b = rt
880 .create_entity(
881 &token,
882 "concept",
883 None,
884 "Twin Record",
885 Some("khive Twin Record document"),
886 None,
887 vec![],
888 )
889 .await
890 .expect("create entity b");
891
892 let resolution = resolve_reference(&rt, &ring, &token, "Twin Record", 5, None)
897 .await
898 .expect("resolve_reference");
899
900 match resolution {
901 ReferenceResolution::Ambiguous { candidates } => {
902 let ids: std::collections::HashSet<Uuid> =
903 candidates.iter().map(|c| c.id).collect();
904 assert!(ids.contains(&a.id) && ids.contains(&b.id));
905 }
906 other => panic!("expected Ambiguous on a genuine name tie, got {other:?}"),
907 }
908 }
909
910 #[tokio::test]
911 async fn fallback_stage_resolves_high_similarity_semantic_only_candidate() {
912 let rt = runtime_with_constant_embeddings();
913 let token = actor_token("resolver-test");
914 let ring = ReferenceRing::new();
915
916 let entity = rt
917 .create_entity(&token, "concept", None, "Canine", None, None, vec![])
918 .await
919 .expect("create semantic match");
920
921 let raw_hits = rt
922 .vector_search(
923 &token,
924 None,
925 Some("domestic dog"),
926 5,
927 Some(SubstrateKind::Entity),
928 )
929 .await
930 .expect("vector search");
931 assert_eq!(raw_hits.len(), 1);
932 assert_eq!(raw_hits[0].subject_id, entity.id);
933 assert!((raw_hits[0].score.to_f64() - 1.0).abs() < 1e-6);
936
937 let hits = rt
938 .hybrid_search(&token, "domestic dog", None, 5, None, None, &[], None)
939 .await
940 .expect("hybrid search");
941 assert_eq!(hits.len(), 1);
942 assert_eq!(hits[0].source, SearchSource::Vector);
943
944 let resolution = resolve_reference(&rt, &ring, &token, "domestic dog", 5, None)
945 .await
946 .expect("resolve_reference");
947
948 assert_eq!(
949 resolution,
950 ReferenceResolution::Resolved {
951 id: entity.id,
952 confidence: SEARCH_RESOLVED_CONFIDENCE,
953 }
954 );
955 }
956
957 #[tokio::test]
958 async fn fallback_stage_resolves_moderate_similarity_semantic_only_candidate() {
959 let rt = runtime_with_constant_embeddings();
964 let token = actor_token("resolver-test");
965 let ring = ReferenceRing::new();
966 let dimensions = EmbeddingModel::AllMiniLmL6V2.dimensions();
967
968 let entity = rt
969 .create_entity(
970 &token,
971 "concept",
972 None,
973 "Moderately Similar Candidate",
974 None,
975 None,
976 vec![],
977 )
978 .await
979 .expect("create moderate-similarity entity");
980 let vectors = rt.vectors(&token).expect("vector store");
981 vectors
982 .delete(entity.id)
983 .await
984 .expect("delete generated vector");
985 let aligned = dimensions * 3 / 4;
986 let mut moderate_vector = vec![1.0f32; aligned];
987 moderate_vector.extend(vec![-1.0f32; dimensions - aligned]);
988 vectors
989 .insert(
990 entity.id,
991 SubstrateKind::Entity,
992 token.namespace().as_str(),
993 "entity.body",
994 vec![moderate_vector],
995 )
996 .await
997 .expect("insert moderate-similarity vector");
998
999 let query = "totally-nonexistent-moderate-zzz";
1000 let raw_hits = rt
1001 .vector_search(&token, None, Some(query), 5, Some(SubstrateKind::Entity))
1002 .await
1003 .expect("vector search");
1004 assert_eq!(raw_hits.len(), 1);
1005 assert_eq!(raw_hits[0].subject_id, entity.id);
1006 assert!((raw_hits[0].score.to_f64() - 0.5).abs() < 1e-6);
1007
1008 let resolution = resolve_reference(&rt, &ring, &token, query, 5, None)
1009 .await
1010 .expect("resolve_reference");
1011
1012 assert_eq!(
1013 resolution,
1014 ReferenceResolution::Resolved {
1015 id: entity.id,
1016 confidence: SEARCH_RESOLVED_CONFIDENCE,
1017 }
1018 );
1019 }
1020
1021 #[tokio::test]
1022 async fn fallback_stage_drops_orthogonal_semantic_only_candidate() {
1023 let rt = runtime_with_constant_embeddings();
1027 let token = actor_token("resolver-test");
1028 let ring = ReferenceRing::new();
1029 let dimensions = EmbeddingModel::AllMiniLmL6V2.dimensions();
1030
1031 let entity = rt
1032 .create_entity(
1033 &token,
1034 "concept",
1035 None,
1036 "Unrelated Orthogonal",
1037 None,
1038 None,
1039 vec![],
1040 )
1041 .await
1042 .expect("create unrelated entity");
1043 let vectors = rt.vectors(&token).expect("vector store");
1044 vectors
1045 .delete(entity.id)
1046 .await
1047 .expect("delete generated vector");
1048 let mut orthogonal_vector = vec![1.0f32; dimensions / 2];
1049 orthogonal_vector.extend(vec![-1.0f32; dimensions - dimensions / 2]);
1050 vectors
1051 .insert(
1052 entity.id,
1053 SubstrateKind::Entity,
1054 token.namespace().as_str(),
1055 "entity.body",
1056 vec![orthogonal_vector],
1057 )
1058 .await
1059 .expect("insert orthogonal vector");
1060
1061 let raw_hits = rt
1062 .vector_search(
1063 &token,
1064 None,
1065 Some("totally-nonexistent-orthogonal-zzz"),
1066 5,
1067 Some(SubstrateKind::Entity),
1068 )
1069 .await
1070 .expect("vector search");
1071 assert_eq!(raw_hits.len(), 1);
1072 assert!((raw_hits[0].score.to_f64() - 0.0).abs() < 1e-6);
1073
1074 let resolution = resolve_reference(
1075 &rt,
1076 &ring,
1077 &token,
1078 "totally-nonexistent-orthogonal-zzz",
1079 5,
1080 None,
1081 )
1082 .await
1083 .expect("resolve_reference");
1084
1085 assert_eq!(resolution, ReferenceResolution::NotFound);
1086 }
1087
1088 #[tokio::test]
1089 async fn fallback_stage_drops_low_similarity_semantic_only_candidates() {
1090 let rt = runtime_with_constant_embeddings();
1091 let token = actor_token("resolver-test");
1092 let ring = ReferenceRing::new();
1093 let dimensions = EmbeddingModel::AllMiniLmL6V2.dimensions();
1094
1095 let entity = rt
1096 .create_entity(
1097 &token,
1098 "concept",
1099 None,
1100 "Unrelated Alpha",
1101 None,
1102 None,
1103 vec![],
1104 )
1105 .await
1106 .expect("create unrelated entity");
1107 let vectors = rt.vectors(&token).expect("vector store");
1108 vectors
1109 .delete(entity.id)
1110 .await
1111 .expect("delete generated vector");
1112 let quarter = dimensions / 4;
1118 let mut mismatched_vector = vec![1.0f32; quarter];
1119 mismatched_vector.extend(vec![-1.0f32; dimensions - quarter]);
1120 vectors
1121 .insert(
1122 entity.id,
1123 SubstrateKind::Entity,
1124 token.namespace().as_str(),
1125 "entity.body",
1126 vec![mismatched_vector],
1127 )
1128 .await
1129 .expect("insert mismatched vector");
1130
1131 let raw_hits = rt
1132 .vector_search(
1133 &token,
1134 None,
1135 Some("totally-nonexistent-zzz"),
1136 5,
1137 Some(SubstrateKind::Entity),
1138 )
1139 .await
1140 .expect("vector search");
1141 assert_eq!(raw_hits.len(), 1);
1142 assert!((raw_hits[0].score.to_f64() + 0.5).abs() < 1e-6);
1143
1144 let resolution = resolve_reference(&rt, &ring, &token, "totally-nonexistent-zzz", 5, None)
1145 .await
1146 .expect("resolve_reference");
1147
1148 assert_eq!(resolution, ReferenceResolution::NotFound);
1149 }
1150
1151 #[tokio::test]
1155 async fn fallback_stage_still_not_found_on_garbage() {
1156 let rt = KhiveRuntime::memory().expect("in-memory runtime");
1157 let token = actor_token("resolver-test");
1158 let ring = ReferenceRing::new();
1159
1160 rt.create_entity(
1161 &token,
1162 "concept",
1163 None,
1164 "ADR-040",
1165 Some("khive ADR-040"),
1166 None,
1167 vec![],
1168 )
1169 .await
1170 .expect("create unrelated entity");
1171
1172 let resolution = resolve_reference(
1173 &rt,
1174 &ring,
1175 &token,
1176 "zzqxw completely unrelated garbage nonsense",
1177 5,
1178 None,
1179 )
1180 .await
1181 .expect("resolve_reference");
1182 assert_eq!(resolution, ReferenceResolution::NotFound);
1183 }
1184}