kiromi-ai-cli 0.2.2

Operator and developer CLI for the kiromi-ai-memory store: append, search, snapshot, regenerate, migrate-scheme, gc, audit-tail.
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Build the embedder registry from compiled-in features and resolve
//! `--embedder-family` โ†’ `Box<dyn Embedder>`.

use kiromi_ai_memory::{Embedder, EmbedderRegistry};

use crate::config::EmbedderConfig;
use crate::error::{CliError, ExitCode};

/// Build a registry pre-populated with every embedder family compiled in.
#[must_use]
pub(crate) fn build_registry() -> EmbedderRegistry {
    let mut r = EmbedderRegistry::empty();
    #[cfg(feature = "embed-onnx")]
    {
        kiromi_ai_embed_onnx::register(&mut r);
    }
    // Always-registered "mock" family โ€” exercised by `tests/*.rs` and operators
    // can opt into it for smoke testing without pulling an ONNX model. Spec ยง 12.4.
    register_mock(&mut r);
    r
}

/// Build an embedder from the resolved `[embedder]` block.
///
/// Returns `Ok(None)` when the caller passed `--no-embedder` (signalled by
/// `cfg = None`). Returns `Err(Config)` when the family is unknown โ€” with
/// a helpful hint when `"onnx"` is requested but the binary was compiled
/// without the `embed-onnx` feature.
pub(crate) async fn resolve(
    cfg: Option<&EmbedderConfig>,
) -> Result<Option<Box<dyn Embedder>>, CliError> {
    let Some(cfg) = cfg else {
        return Ok(None);
    };
    // Surface a precise error before we even consult the registry: the
    // default install path no longer pulls `embed-onnx`, so callers asking
    // for the bundled ONNX adapter need a hint, not a generic "family not
    // registered" message.
    if cfg.family == "onnx" && !cfg!(feature = "embed-onnx") {
        return Err(CliError {
            kind: ExitCode::Config,
            source: anyhow::anyhow!(
                "the 'onnx' embedder family is not compiled in. Reinstall with \
                 `cargo install kiromi-ai-cli --features embed-onnx`, or use \
                 `--no-embedder` to manage embeddings outside the engine."
            ),
        });
    }
    let registry = build_registry();
    let e = registry
        .build(&cfg.family, cfg.config.clone())
        .await
        .map_err(CliError::from)?;
    Ok(Some(e))
}

fn register_mock(r: &mut EmbedderRegistry) {
    r.register("mock", |_cfg| async {
        let e: Box<dyn Embedder> = Box::new(kiromi_ai_test_suite::MockEmbedder::new());
        Ok::<_, kiromi_ai_memory::Error>(e)
    });
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::ExitCode;

    #[tokio::test]
    async fn mock_family_resolves() {
        let cfg = EmbedderConfig {
            family: "mock".into(),
            config: serde_json::Value::Null,
        };
        let e = resolve(Some(&cfg)).await.unwrap().unwrap();
        // MockEmbedder ID is documented in the test suite.
        assert!(!e.id().is_empty());
    }

    #[tokio::test]
    async fn no_embedder_returns_none() {
        let none = resolve(None).await.unwrap();
        assert!(none.is_none());
    }

    #[tokio::test]
    async fn unknown_family_is_config_error() {
        let cfg = EmbedderConfig {
            family: "no-such".into(),
            config: serde_json::Value::Null,
        };
        let err = resolve(Some(&cfg)).await.unwrap_err();
        assert_eq!(err.kind, ExitCode::Config);
    }
}