use std::path::PathBuf;
use anyhow::{anyhow, Result};
use clap::Subcommand;
use serde::Serialize;
#[derive(Subcommand, Debug)]
pub enum EngineCommand {
List(EngineListArgs),
Status(EngineStatusArgs),
Migrate(EngineMigrateArgs),
DriftCheck(EngineDriftCheckArgs),
}
#[derive(clap::Parser, Debug)]
pub struct EngineListArgs {
#[arg(long)]
pub human: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(clap::Parser, Debug)]
pub struct EngineStatusArgs {
pub engine: String,
#[arg(long)]
pub human: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(clap::Parser, Debug)]
pub struct EngineMigrateArgs {
pub engine: String,
#[arg(long, conflicts_with_all = &["resume", "abort"])]
pub to: Option<String>,
#[arg(long, conflicts_with_all = &["to", "abort"])]
pub resume: bool,
#[arg(long, conflicts_with_all = &["to", "resume"])]
pub abort: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(clap::Parser, Debug)]
pub struct EngineDriftCheckArgs {
pub engine: String,
#[arg(long, default_value = "1000")]
pub sample: usize,
#[arg(long)]
pub human: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(Clone, Debug, Serialize)]
pub struct EngineModelRecord {
pub engine_name: String,
pub model_id: String,
pub key_version: String,
pub dimensions: u32,
pub status: String,
pub activated_at: Option<i64>,
pub superseded_at: Option<i64>,
}
#[derive(Debug, Serialize)]
pub struct EngineStatus {
pub engine_name: String,
pub active_model: Option<EngineModelRecord>,
pub migration_in_progress: bool,
pub pending_model: Option<EngineModelRecord>,
}
pub fn run_engine(cmd: EngineCommand) -> Result<()> {
match cmd {
EngineCommand::List(args) => cmd_engine_list(args),
EngineCommand::Status(args) => cmd_engine_status(args),
EngineCommand::Migrate(args) => cmd_engine_migrate(args),
EngineCommand::DriftCheck(args) => cmd_engine_drift_check(args),
}
}
fn cmd_engine_list(args: EngineListArgs) -> Result<()> {
let records = query_embedding_models(args.db.as_deref(), None)?;
if args.human {
for r in &records {
println!(
" {:<20} model={:<30} status={} key_version={} dim={}",
r.engine_name, r.model_id, r.status, r.key_version, r.dimensions
);
}
} else {
let json = serde_json::to_string(&records).expect("serialize EngineModelRecord[]");
println!("{json}");
}
Ok(())
}
fn cmd_engine_status(args: EngineStatusArgs) -> Result<()> {
let all = query_embedding_models(args.db.as_deref(), Some(&args.engine))?;
let active = all.iter().find(|r| r.status == "active").cloned();
let pending = all.iter().find(|r| r.status == "pending").cloned();
let status = EngineStatus {
engine_name: args.engine.clone(),
migration_in_progress: pending.is_some(),
active_model: active,
pending_model: pending,
};
if args.human {
if let Some(ref m) = status.active_model {
println!("engine: {}", status.engine_name);
println!(" active model: {}", m.model_id);
println!(" key_version: {}", m.key_version);
println!(" dimensions: {}", m.dimensions);
println!(" migration_in_progress:{}", status.migration_in_progress);
} else {
println!(
"engine: {} — no active model registered",
status.engine_name
);
}
} else {
let json = serde_json::to_string(&status).expect("serialize EngineStatus");
println!("{json}");
}
Ok(())
}
fn cmd_engine_migrate(_args: EngineMigrateArgs) -> Result<()> {
Err(anyhow!(
"engine migrate is not yet implemented (ADR-043 D2-D6 — EmbedMigrationWorker deferred \
to follow-up #380). Use 'kkernel engine list' / 'status' to inspect registered models."
))
}
fn cmd_engine_drift_check(_args: EngineDriftCheckArgs) -> Result<()> {
Err(anyhow!(
"engine drift-check is not yet implemented (ADR-043 §5 lattice_transport integration \
deferred). Track follow-up #380."
))
}
fn query_embedding_models(
_db: Option<&std::path::Path>,
engine_filter: Option<&str>,
) -> Result<Vec<EngineModelRecord>> {
if let Some(engine) = engine_filter {
tracing::debug!(
engine,
"query_embedding_models: _embedding_models not yet populated"
);
} else {
tracing::debug!("query_embedding_models: _embedding_models not yet populated");
}
Ok(Vec::new())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn engine_list_empty_ok() {
let args = EngineListArgs {
human: false,
db: None,
};
cmd_engine_list(args).expect("engine list succeeds on empty registry");
}
#[test]
fn engine_status_empty_ok() {
let args = EngineStatusArgs {
engine: "mE5-small".into(),
human: false,
db: None,
};
cmd_engine_status(args).expect("engine status succeeds on empty registry");
}
#[test]
fn engine_migrate_returns_not_implemented() {
let args = EngineMigrateArgs {
engine: "mE5-small".into(),
to: Some("bge-small-en-v1.5".into()),
resume: false,
abort: false,
db: None,
};
let err = cmd_engine_migrate(args).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not yet implemented"),
"expected 'not yet implemented' in error, got: {msg}"
);
assert!(
msg.contains("#380"),
"expected follow-up issue reference in error, got: {msg}"
);
}
#[test]
fn engine_migrate_resume_returns_not_implemented() {
let args = EngineMigrateArgs {
engine: "mE5-small".into(),
to: None,
resume: true,
abort: false,
db: None,
};
let err = cmd_engine_migrate(args).unwrap_err();
assert!(err.to_string().contains("not yet implemented"));
}
#[test]
fn engine_migrate_abort_returns_not_implemented() {
let args = EngineMigrateArgs {
engine: "mE5-small".into(),
to: None,
resume: false,
abort: true,
db: None,
};
let err = cmd_engine_migrate(args).unwrap_err();
assert!(err.to_string().contains("not yet implemented"));
}
#[test]
fn drift_check_returns_not_implemented() {
let args = EngineDriftCheckArgs {
engine: "mE5-small".into(),
sample: 500,
human: false,
db: None,
};
let err = cmd_engine_drift_check(args).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not yet implemented"),
"expected 'not yet implemented' in error, got: {msg}"
);
assert!(
msg.contains("#380"),
"expected follow-up issue reference in error, got: {msg}"
);
}
}