From 35fec9ff4b9ac6c528ea324cef06c12b504f3f54 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Fri, 22 May 2026 12:34:32 -0400 Subject: [PATCH 1/2] feat(fold): add ObjectiveRegistry for dynamic objective dispatch Ports ObjectiveRegistry from khive-internal, adapted to OSS Selection.item API. Provides thread-safe registration and lookup of named objective functions for fold composition. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/khive-fold/Cargo.toml | 1 + crates/khive-fold/src/objective/mod.rs | 2 + crates/khive-fold/src/objective/registry.rs | 275 ++++++++++++++++++++ 3 files changed, 278 insertions(+) create mode 100644 crates/khive-fold/src/objective/registry.rs diff --git a/crates/khive-fold/Cargo.toml b/crates/khive-fold/Cargo.toml index eebf36dc..54868b4d 100644 --- a/crates/khive-fold/Cargo.toml +++ b/crates/khive-fold/Cargo.toml @@ -17,4 +17,5 @@ serde_json = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } thiserror = { workspace = true } +parking_lot = { workspace = true } diff --git a/crates/khive-fold/src/objective/mod.rs b/crates/khive-fold/src/objective/mod.rs index c4504982..e2040fb6 100644 --- a/crates/khive-fold/src/objective/mod.rs +++ b/crates/khive-fold/src/objective/mod.rs @@ -4,11 +4,13 @@ pub mod builtin; pub mod compose; mod context; pub mod error; +pub mod registry; mod selection; mod traits; pub use context::ObjectiveContext; pub use error::{ObjectiveError, ObjectiveResult}; +pub use registry::{ObjectiveRegistry, RegisteredObjective}; pub use selection::Selection; pub use traits::{objective_fn, DeterministicObjective, Objective}; diff --git a/crates/khive-fold/src/objective/registry.rs b/crates/khive-fold/src/objective/registry.rs new file mode 100644 index 00000000..4ce97815 --- /dev/null +++ b/crates/khive-fold/src/objective/registry.rs @@ -0,0 +1,275 @@ +//! Objective registry for dynamic dispatch. + +use std::collections::HashMap; +use std::sync::Arc; + +use parking_lot::RwLock; + +use crate::{Objective, ObjectiveContext, ObjectiveError, ObjectiveResult, Selection}; + +/// A type-erased objective wrapper. +pub struct RegisteredObjective { + /// Name of the objective + pub name: String, + /// Description + pub description: Option, + /// The objective implementation + objective: Box>, +} + +impl RegisteredObjective { + /// Create a new registered objective + pub fn new(name: impl Into, objective: Box>) -> Self { + Self { + name: name.into(), + description: None, + objective, + } + } + + /// Add a description + pub fn with_description(mut self, desc: impl Into) -> Self { + self.description = Some(desc.into()); + self + } + + /// Score a candidate + pub fn score(&self, candidate: &T, context: &ObjectiveContext) -> f64 { + self.objective.score(candidate, context) + } + + /// Select from candidates + pub fn select<'a>( + &self, + candidates: &'a [T], + context: &ObjectiveContext, + ) -> ObjectiveResult> { + self.objective.select(candidates, context) + } +} + +/// Registry of named objectives. +pub struct ObjectiveRegistry { + objectives: RwLock>>>, + default: RwLock>, +} + +impl Default for ObjectiveRegistry { + fn default() -> Self { + Self::new() + } +} + +impl ObjectiveRegistry { + /// Create a new empty registry + pub fn new() -> Self { + Self { + objectives: RwLock::new(HashMap::new()), + default: RwLock::new(None), + } + } + + /// Register an objective. + /// + /// Returns the previously registered objective if one existed with the same name. + pub fn register( + &self, + name: impl Into, + objective: Box>, + ) -> Option>> { + let name = name.into(); + let registered = Arc::new(RegisteredObjective::new(name.clone(), objective)); + + let mut objectives = self.objectives.write(); + objectives.insert(name, registered) + } + + /// Register an objective with description. + /// + /// Returns the previously registered objective if one existed with the same name. + pub fn register_with_desc( + &self, + name: impl Into, + description: impl Into, + objective: Box>, + ) -> Option>> { + let name = name.into(); + let registered = Arc::new( + RegisteredObjective::new(name.clone(), objective).with_description(description), + ); + + let mut objectives = self.objectives.write(); + objectives.insert(name, registered) + } + + /// Set the default objective + pub fn set_default(&self, name: impl Into) -> ObjectiveResult<()> { + let name = name.into(); + + let objectives = self.objectives.read(); + if !objectives.contains_key(&name) { + return Err(ObjectiveError::NotFound(name)); + } + drop(objectives); + + let mut default = self.default.write(); + *default = Some(name); + Ok(()) + } + + /// Get an objective by name + pub fn get(&self, name: &str) -> ObjectiveResult>> { + let objectives = self.objectives.read(); + objectives + .get(name) + .cloned() + .ok_or_else(|| ObjectiveError::NotFound(name.to_string())) + } + + /// Get the default objective + pub fn get_default(&self) -> ObjectiveResult>> { + let default = self.default.read(); + match default.as_ref() { + Some(name) => { + let name: String = name.clone(); + drop(default); + self.get(&name) + } + None => Err(ObjectiveError::NotFound("No default set".to_string())), + } + } + + /// List all registered objective names. + /// + /// Returns names in sorted order for deterministic output. + pub fn list(&self) -> Vec { + let objectives = self.objectives.read(); + let mut names: Vec = objectives.keys().cloned().collect(); + names.sort(); + names + } + + /// Check if an objective is registered + pub fn contains(&self, name: &str) -> bool { + let objectives = self.objectives.read(); + objectives.contains_key(name) + } + + /// Score using a named objective + pub fn score( + &self, + name: &str, + candidate: &T, + context: &ObjectiveContext, + ) -> ObjectiveResult { + let objective = self.get(name)?; + Ok(objective.score(candidate, context)) + } + + /// Select using a named objective + pub fn select<'a>( + &self, + name: &str, + candidates: &'a [T], + context: &ObjectiveContext, + ) -> ObjectiveResult> { + let objective = self.get(name)?; + objective.select(candidates, context) + } + + /// Select using the default objective + pub fn select_default<'a>( + &self, + candidates: &'a [T], + context: &ObjectiveContext, + ) -> ObjectiveResult> { + let objective = self.get_default()?; + objective.select(candidates, context) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::objective_fn; + + #[test] + fn test_register_and_get() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + let old = registry.register("max", Box::new(obj)); + + assert!(old.is_none()); + assert!(registry.contains("max")); + assert!(!registry.contains("min")); + } + + #[test] + fn test_register_overwrites() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + + let obj1 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + let obj2 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| -(*n as f64)); + + let old1 = registry.register("test", Box::new(obj1)); + assert!(old1.is_none()); + + let old2 = registry.register("test", Box::new(obj2)); + assert!(old2.is_some()); + + let candidates = vec![1, 5, 3]; + let selection = registry + .select("test", &candidates, &ObjectiveContext::new()) + .unwrap(); + assert_eq!(*selection.item, 1); + } + + #[test] + fn test_select_by_name() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + registry.register("max", Box::new(obj)); + + let candidates = vec![1, 5, 3]; + let selection = registry + .select("max", &candidates, &ObjectiveContext::new()) + .unwrap(); + + assert_eq!(*selection.item, 5); + } + + #[test] + fn test_default_objective() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + registry.register("max", Box::new(obj)); + registry.set_default("max").unwrap(); + + let candidates = vec![1, 5, 3]; + let selection = registry + .select_default(&candidates, &ObjectiveContext::new()) + .unwrap(); + + assert_eq!(*selection.item, 5); + } + + #[test] + fn test_list_objectives_sorted() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + + let obj1 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + let obj2 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| -(*n as f64)); + let obj3 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| (*n as f64).abs()); + + registry.register("zebra", Box::new(obj1)); + registry.register("alpha", Box::new(obj2)); + registry.register("middle", Box::new(obj3)); + + let names = registry.list(); + assert_eq!(names.len(), 3); + assert_eq!(names, vec!["alpha", "middle", "zebra"]); + } +} From 9d599bdf7f171c07f10aa2bc83936076c87c8904 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Fri, 22 May 2026 12:57:53 -0400 Subject: [PATCH 2/2] fix(runtime): relocate ObjectiveRegistry from khive-fold to khive-runtime Per ADR-058, ObjectiveRegistry is runtime infrastructure (named lookup, concurrent mutable state), not fold algebra. Fixes from critic review: - Single RwLock over combined RegistryInner (fixes TOCTOU in set_default) - T: Send + Sync bounds on all public types - Debug impls for RegisteredObjective and ObjectiveRegistry - Documented score() as raw (no precision weighting per ADR-059) - Added error path tests (get/get_default/set_default on missing) - Added concurrent read/write test (validates RwLock) - Removed parking_lot from khive-fold (foundation stays pure math) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/khive-fold/Cargo.toml | 1 - crates/khive-fold/src/objective/mod.rs | 2 - crates/khive-fold/src/objective/registry.rs | 275 ---------------- crates/khive-runtime/Cargo.toml | 1 + crates/khive-runtime/src/lib.rs | 2 + crates/khive-runtime/src/registry.rs | 346 ++++++++++++++++++++ 6 files changed, 349 insertions(+), 278 deletions(-) delete mode 100644 crates/khive-fold/src/objective/registry.rs create mode 100644 crates/khive-runtime/src/registry.rs diff --git a/crates/khive-fold/Cargo.toml b/crates/khive-fold/Cargo.toml index 54868b4d..eebf36dc 100644 --- a/crates/khive-fold/Cargo.toml +++ b/crates/khive-fold/Cargo.toml @@ -17,5 +17,4 @@ serde_json = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } thiserror = { workspace = true } -parking_lot = { workspace = true } diff --git a/crates/khive-fold/src/objective/mod.rs b/crates/khive-fold/src/objective/mod.rs index e2040fb6..c4504982 100644 --- a/crates/khive-fold/src/objective/mod.rs +++ b/crates/khive-fold/src/objective/mod.rs @@ -4,13 +4,11 @@ pub mod builtin; pub mod compose; mod context; pub mod error; -pub mod registry; mod selection; mod traits; pub use context::ObjectiveContext; pub use error::{ObjectiveError, ObjectiveResult}; -pub use registry::{ObjectiveRegistry, RegisteredObjective}; pub use selection::Selection; pub use traits::{objective_fn, DeterministicObjective, Objective}; diff --git a/crates/khive-fold/src/objective/registry.rs b/crates/khive-fold/src/objective/registry.rs deleted file mode 100644 index 4ce97815..00000000 --- a/crates/khive-fold/src/objective/registry.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! Objective registry for dynamic dispatch. - -use std::collections::HashMap; -use std::sync::Arc; - -use parking_lot::RwLock; - -use crate::{Objective, ObjectiveContext, ObjectiveError, ObjectiveResult, Selection}; - -/// A type-erased objective wrapper. -pub struct RegisteredObjective { - /// Name of the objective - pub name: String, - /// Description - pub description: Option, - /// The objective implementation - objective: Box>, -} - -impl RegisteredObjective { - /// Create a new registered objective - pub fn new(name: impl Into, objective: Box>) -> Self { - Self { - name: name.into(), - description: None, - objective, - } - } - - /// Add a description - pub fn with_description(mut self, desc: impl Into) -> Self { - self.description = Some(desc.into()); - self - } - - /// Score a candidate - pub fn score(&self, candidate: &T, context: &ObjectiveContext) -> f64 { - self.objective.score(candidate, context) - } - - /// Select from candidates - pub fn select<'a>( - &self, - candidates: &'a [T], - context: &ObjectiveContext, - ) -> ObjectiveResult> { - self.objective.select(candidates, context) - } -} - -/// Registry of named objectives. -pub struct ObjectiveRegistry { - objectives: RwLock>>>, - default: RwLock>, -} - -impl Default for ObjectiveRegistry { - fn default() -> Self { - Self::new() - } -} - -impl ObjectiveRegistry { - /// Create a new empty registry - pub fn new() -> Self { - Self { - objectives: RwLock::new(HashMap::new()), - default: RwLock::new(None), - } - } - - /// Register an objective. - /// - /// Returns the previously registered objective if one existed with the same name. - pub fn register( - &self, - name: impl Into, - objective: Box>, - ) -> Option>> { - let name = name.into(); - let registered = Arc::new(RegisteredObjective::new(name.clone(), objective)); - - let mut objectives = self.objectives.write(); - objectives.insert(name, registered) - } - - /// Register an objective with description. - /// - /// Returns the previously registered objective if one existed with the same name. - pub fn register_with_desc( - &self, - name: impl Into, - description: impl Into, - objective: Box>, - ) -> Option>> { - let name = name.into(); - let registered = Arc::new( - RegisteredObjective::new(name.clone(), objective).with_description(description), - ); - - let mut objectives = self.objectives.write(); - objectives.insert(name, registered) - } - - /// Set the default objective - pub fn set_default(&self, name: impl Into) -> ObjectiveResult<()> { - let name = name.into(); - - let objectives = self.objectives.read(); - if !objectives.contains_key(&name) { - return Err(ObjectiveError::NotFound(name)); - } - drop(objectives); - - let mut default = self.default.write(); - *default = Some(name); - Ok(()) - } - - /// Get an objective by name - pub fn get(&self, name: &str) -> ObjectiveResult>> { - let objectives = self.objectives.read(); - objectives - .get(name) - .cloned() - .ok_or_else(|| ObjectiveError::NotFound(name.to_string())) - } - - /// Get the default objective - pub fn get_default(&self) -> ObjectiveResult>> { - let default = self.default.read(); - match default.as_ref() { - Some(name) => { - let name: String = name.clone(); - drop(default); - self.get(&name) - } - None => Err(ObjectiveError::NotFound("No default set".to_string())), - } - } - - /// List all registered objective names. - /// - /// Returns names in sorted order for deterministic output. - pub fn list(&self) -> Vec { - let objectives = self.objectives.read(); - let mut names: Vec = objectives.keys().cloned().collect(); - names.sort(); - names - } - - /// Check if an objective is registered - pub fn contains(&self, name: &str) -> bool { - let objectives = self.objectives.read(); - objectives.contains_key(name) - } - - /// Score using a named objective - pub fn score( - &self, - name: &str, - candidate: &T, - context: &ObjectiveContext, - ) -> ObjectiveResult { - let objective = self.get(name)?; - Ok(objective.score(candidate, context)) - } - - /// Select using a named objective - pub fn select<'a>( - &self, - name: &str, - candidates: &'a [T], - context: &ObjectiveContext, - ) -> ObjectiveResult> { - let objective = self.get(name)?; - objective.select(candidates, context) - } - - /// Select using the default objective - pub fn select_default<'a>( - &self, - candidates: &'a [T], - context: &ObjectiveContext, - ) -> ObjectiveResult> { - let objective = self.get_default()?; - objective.select(candidates, context) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::objective_fn; - - #[test] - fn test_register_and_get() { - let registry: ObjectiveRegistry = ObjectiveRegistry::new(); - - let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); - let old = registry.register("max", Box::new(obj)); - - assert!(old.is_none()); - assert!(registry.contains("max")); - assert!(!registry.contains("min")); - } - - #[test] - fn test_register_overwrites() { - let registry: ObjectiveRegistry = ObjectiveRegistry::new(); - - let obj1 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); - let obj2 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| -(*n as f64)); - - let old1 = registry.register("test", Box::new(obj1)); - assert!(old1.is_none()); - - let old2 = registry.register("test", Box::new(obj2)); - assert!(old2.is_some()); - - let candidates = vec![1, 5, 3]; - let selection = registry - .select("test", &candidates, &ObjectiveContext::new()) - .unwrap(); - assert_eq!(*selection.item, 1); - } - - #[test] - fn test_select_by_name() { - let registry: ObjectiveRegistry = ObjectiveRegistry::new(); - - let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); - registry.register("max", Box::new(obj)); - - let candidates = vec![1, 5, 3]; - let selection = registry - .select("max", &candidates, &ObjectiveContext::new()) - .unwrap(); - - assert_eq!(*selection.item, 5); - } - - #[test] - fn test_default_objective() { - let registry: ObjectiveRegistry = ObjectiveRegistry::new(); - - let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); - registry.register("max", Box::new(obj)); - registry.set_default("max").unwrap(); - - let candidates = vec![1, 5, 3]; - let selection = registry - .select_default(&candidates, &ObjectiveContext::new()) - .unwrap(); - - assert_eq!(*selection.item, 5); - } - - #[test] - fn test_list_objectives_sorted() { - let registry: ObjectiveRegistry = ObjectiveRegistry::new(); - - let obj1 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); - let obj2 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| -(*n as f64)); - let obj3 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| (*n as f64).abs()); - - registry.register("zebra", Box::new(obj1)); - registry.register("alpha", Box::new(obj2)); - registry.register("middle", Box::new(obj3)); - - let names = registry.list(); - assert_eq!(names.len(), 3); - assert_eq!(names, vec!["alpha", "middle", "zebra"]); - } -} diff --git a/crates/khive-runtime/Cargo.toml b/crates/khive-runtime/Cargo.toml index 9093b839..e3623dd1 100644 --- a/crates/khive-runtime/Cargo.toml +++ b/crates/khive-runtime/Cargo.toml @@ -28,6 +28,7 @@ chrono = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } lattice-embed = { workspace = true } +parking_lot = { workspace = true } rusqlite = { version = "0.33" } tempfile = "3" diff --git a/crates/khive-runtime/src/lib.rs b/crates/khive-runtime/src/lib.rs index 3cec2b3b..4e39ffda 100644 --- a/crates/khive-runtime/src/lib.rs +++ b/crates/khive-runtime/src/lib.rs @@ -26,6 +26,7 @@ pub mod fusion; pub mod graph_traversal; pub mod objectives; pub mod operations; +pub mod registry; pub mod pack; pub mod portability; pub mod retrieval; @@ -44,6 +45,7 @@ pub use objectives::{ VectorSimilarityObjective, }; pub use operations::{NoteSearchHit, QueryResult, Resolved}; +pub use registry::{ObjectiveRegistry, RegisteredObjective}; pub use pack::{ DispatchHook, KindHook, PackFactory, PackRegistration, PackRegistry, PackRuntime, VerbRegistry, VerbRegistryBuilder, diff --git a/crates/khive-runtime/src/registry.rs b/crates/khive-runtime/src/registry.rs new file mode 100644 index 00000000..cfab6f1c --- /dev/null +++ b/crates/khive-runtime/src/registry.rs @@ -0,0 +1,346 @@ +//! Objective registry for dynamic dispatch. +//! +//! Runtime infrastructure: named registration, lookup, defaults. +//! Lives in khive-runtime (not khive-fold) per ADR-058. + +use std::collections::HashMap; +use std::fmt; +use std::sync::Arc; + +use parking_lot::RwLock; + +use khive_fold::objective::{Objective, ObjectiveContext, ObjectiveError, ObjectiveResult, Selection}; + +/// A type-erased objective wrapper. +pub struct RegisteredObjective { + pub name: String, + pub description: Option, + objective: Box>, +} + +impl fmt::Debug for RegisteredObjective { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RegisteredObjective") + .field("name", &self.name) + .field("description", &self.description) + .finish_non_exhaustive() + } +} + +impl RegisteredObjective { + pub fn new(name: impl Into, objective: Box>) -> Self { + Self { + name: name.into(), + description: None, + objective, + } + } + + pub fn with_description(mut self, desc: impl Into) -> Self { + self.description = Some(desc.into()); + self + } + + /// Raw score (no precision weighting). Use `select()` for ranked selection + /// that applies `score * precision` per ADR-059. + pub fn score(&self, candidate: &T, context: &ObjectiveContext) -> f64 { + self.objective.score(candidate, context) + } + + pub fn select<'a>( + &self, + candidates: &'a [T], + context: &ObjectiveContext, + ) -> ObjectiveResult> { + self.objective.select(candidates, context) + } +} + +struct RegistryInner { + objectives: HashMap>>, + default: Option, +} + +/// Registry of named objectives. +/// +/// Thread-safe: all operations are behind a single `RwLock`. +pub struct ObjectiveRegistry { + inner: RwLock>, +} + +impl fmt::Debug for ObjectiveRegistry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = self.inner.read(); + f.debug_struct("ObjectiveRegistry") + .field("count", &inner.objectives.len()) + .field("default", &inner.default) + .finish() + } +} + +impl Default for ObjectiveRegistry { + fn default() -> Self { + Self::new() + } +} + +impl ObjectiveRegistry { + pub fn new() -> Self { + Self { + inner: RwLock::new(RegistryInner { + objectives: HashMap::new(), + default: None, + }), + } + } + + pub fn register( + &self, + name: impl Into, + objective: Box>, + ) -> Option>> { + let name = name.into(); + let registered = Arc::new(RegisteredObjective::new(name.clone(), objective)); + self.inner.write().objectives.insert(name, registered) + } + + pub fn register_with_desc( + &self, + name: impl Into, + description: impl Into, + objective: Box>, + ) -> Option>> { + let name = name.into(); + let registered = Arc::new( + RegisteredObjective::new(name.clone(), objective).with_description(description), + ); + self.inner.write().objectives.insert(name, registered) + } + + pub fn set_default(&self, name: impl Into) -> ObjectiveResult<()> { + let name = name.into(); + let mut inner = self.inner.write(); + if !inner.objectives.contains_key(&name) { + return Err(ObjectiveError::NotFound(name)); + } + inner.default = Some(name); + Ok(()) + } + + pub fn get(&self, name: &str) -> ObjectiveResult>> { + self.inner + .read() + .objectives + .get(name) + .cloned() + .ok_or_else(|| ObjectiveError::NotFound(name.to_string())) + } + + pub fn get_default(&self) -> ObjectiveResult>> { + let inner = self.inner.read(); + match inner.default.as_ref() { + Some(name) => inner + .objectives + .get(name) + .cloned() + .ok_or_else(|| ObjectiveError::NotFound(name.clone())), + None => Err(ObjectiveError::NotFound("No default set".to_string())), + } + } + + pub fn list(&self) -> Vec { + let inner = self.inner.read(); + let mut names: Vec = inner.objectives.keys().cloned().collect(); + names.sort(); + names + } + + pub fn contains(&self, name: &str) -> bool { + self.inner.read().objectives.contains_key(name) + } + + /// Raw score via a named objective (no precision weighting). + pub fn score( + &self, + name: &str, + candidate: &T, + context: &ObjectiveContext, + ) -> ObjectiveResult { + let objective = self.get(name)?; + Ok(objective.score(candidate, context)) + } + + pub fn select<'a>( + &self, + name: &str, + candidates: &'a [T], + context: &ObjectiveContext, + ) -> ObjectiveResult> { + let objective = self.get(name)?; + objective.select(candidates, context) + } + + pub fn select_default<'a>( + &self, + candidates: &'a [T], + context: &ObjectiveContext, + ) -> ObjectiveResult> { + let objective = self.get_default()?; + objective.select(candidates, context) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use khive_fold::objective::objective_fn; + + #[test] + fn register_and_get() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + let old = registry.register("max", Box::new(obj)); + assert!(old.is_none()); + assert!(registry.contains("max")); + assert!(!registry.contains("min")); + } + + #[test] + fn register_overwrites() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj1 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + let obj2 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| -(*n as f64)); + assert!(registry.register("test", Box::new(obj1)).is_none()); + assert!(registry.register("test", Box::new(obj2)).is_some()); + + let candidates = vec![1, 5, 3]; + let selection = registry + .select("test", &candidates, &ObjectiveContext::new()) + .unwrap(); + assert_eq!(*selection.item, 1); + } + + #[test] + fn select_by_name() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + registry.register("max", Box::new(obj)); + + let candidates = vec![1, 5, 3]; + let selection = registry + .select("max", &candidates, &ObjectiveContext::new()) + .unwrap(); + assert_eq!(*selection.item, 5); + } + + #[test] + fn default_objective() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + registry.register("max", Box::new(obj)); + registry.set_default("max").unwrap(); + + let candidates = vec![1, 5, 3]; + let selection = registry + .select_default(&candidates, &ObjectiveContext::new()) + .unwrap(); + assert_eq!(*selection.item, 5); + } + + #[test] + fn list_objectives_sorted() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj1 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + let obj2 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| -(*n as f64)); + let obj3 = objective_fn(|n: &i32, _ctx: &ObjectiveContext| (*n as f64).abs()); + registry.register("zebra", Box::new(obj1)); + registry.register("alpha", Box::new(obj2)); + registry.register("middle", Box::new(obj3)); + + let names = registry.list(); + assert_eq!(names, vec!["alpha", "middle", "zebra"]); + } + + #[test] + fn get_nonexistent_returns_error() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let result = registry.get("nope"); + assert!(matches!(result, Err(ObjectiveError::NotFound(ref s)) if s == "nope")); + } + + #[test] + fn get_default_without_setting_returns_error() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let result = registry.get_default(); + assert!(matches!(result, Err(ObjectiveError::NotFound(_)))); + } + + #[test] + fn set_default_nonexistent_returns_error() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let result = registry.set_default("ghost"); + assert!(matches!(result, Err(ObjectiveError::NotFound(ref s)) if s == "ghost")); + } + + #[test] + fn score_via_registry() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64 * 2.0); + registry.register("double", Box::new(obj)); + + let score = registry.score("double", &5, &ObjectiveContext::new()).unwrap(); + assert!((score - 10.0).abs() < 1e-12); + } + + #[test] + fn select_default_via_registry() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| -(*n as f64)); + registry.register("min", Box::new(obj)); + registry.set_default("min").unwrap(); + + let candidates = vec![1, 5, 3]; + let selection = registry + .select_default(&candidates, &ObjectiveContext::new()) + .unwrap(); + assert_eq!(*selection.item, 1); + } + + #[test] + fn debug_impls() { + let registry: ObjectiveRegistry = ObjectiveRegistry::new(); + let obj = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64); + registry.register("test", Box::new(obj)); + let debug = format!("{:?}", registry); + assert!(debug.contains("ObjectiveRegistry")); + assert!(debug.contains("count: 1")); + + let registered = registry.get("test").unwrap(); + let debug = format!("{:?}", registered); + assert!(debug.contains("RegisteredObjective")); + assert!(debug.contains("test")); + } + + #[test] + fn concurrent_read_write() { + let registry = Arc::new(ObjectiveRegistry::::new()); + + std::thread::scope(|s| { + for i in 0..8 { + let reg = Arc::clone(®istry); + s.spawn(move || { + let name = format!("obj_{i}"); + let obj = objective_fn(move |n: &i32, _ctx: &ObjectiveContext| *n as f64 + i as f64); + reg.register(name.clone(), Box::new(obj)); + + assert!(reg.contains(&name)); + + let candidates = vec![1, 2, 3]; + let _ = reg.select(&name, &candidates, &ObjectiveContext::new()); + }); + } + }); + + assert_eq!(registry.list().len(), 8); + } +}