#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use mif_problem::{OutputFormat, ProblemMeta, ToProblem};
const DEFAULT_DB_PATH: &str = ".mif/vectors.db";
#[derive(Parser)]
#[command(
name = "mif-cli",
version,
about = "CLI for the MIF (Modeled Information Format) ecosystem"
)]
struct Cli {
#[arg(long, global = true, value_parser = ["pretty", "json"])]
format: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Validate {
file: PathBuf,
},
Ontology {
#[command(subcommand)]
command: OntologyCommand,
},
Ingest {
file: PathBuf,
#[arg(long)]
db_path: Option<PathBuf>,
},
Search {
query: String,
#[arg(long)]
db_path: Option<PathBuf>,
#[arg(long, default_value_t = 10)]
limit: usize,
},
FindSimilar {
id: String,
#[arg(long)]
db_path: Option<PathBuf>,
#[arg(long, default_value_t = 10)]
limit: usize,
},
CorpusStats {
#[arg(long)]
db_path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum OntologyCommand {
Resolve {
id: String,
#[arg(long)]
ontologies_dir: PathBuf,
},
}
#[derive(Debug, thiserror::Error)]
enum CliError {
#[error("failed to read {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to parse {path} as JSON: {source}")]
Json {
path: String,
#[source]
source: serde_json::Error,
},
#[error(transparent)]
Schema(#[from] mif_schema::MifSchemaError),
#[error(transparent)]
Ontology(#[from] mif_ontology::OntologyError),
#[error(transparent)]
Frontmatter(#[from] mif_frontmatter::FrontmatterError),
#[error(transparent)]
Embed(#[from] mif_embed::EmbedError),
#[error(transparent)]
Store(#[from] mif_store::StoreError),
#[error("no document with id '{0}' has been ingested into this vector store")]
DocumentNotFound(String),
}
impl CliError {
const fn meta(&self) -> ProblemMeta {
match self {
Self::Io { .. } => ProblemMeta {
slug: "mif-cli-io",
version: "v1",
title: "Failed to read an input file",
status: 500,
exit_code: 1,
},
Self::Json { .. } => ProblemMeta {
slug: "mif-cli-invalid-json",
version: "v1",
title: "Input file is not valid JSON",
status: 400,
exit_code: 2,
},
Self::DocumentNotFound(_) => ProblemMeta {
slug: "mif-cli-document-not-found",
version: "v1",
title: "No document with the given id has been ingested",
status: 404,
exit_code: 3,
},
Self::Schema(_)
| Self::Ontology(_)
| Self::Frontmatter(_)
| Self::Embed(_)
| Self::Store(_) => ProblemMeta {
slug: "delegated",
version: "v1",
title: "Delegated error",
status: 500,
exit_code: 1,
},
}
}
}
impl ToProblem for CliError {
fn to_problem(&self) -> mif_problem::ProblemDetails {
match self {
Self::Schema(inner) => inner.to_problem(),
Self::Ontology(inner) => inner.to_problem(),
Self::Frontmatter(inner) => inner.to_problem(),
Self::Embed(inner) => inner.to_problem(),
Self::Store(inner) => inner.to_problem(),
Self::Io { source, .. } => {
let (status, fix, action) = mif_problem::classify_io_error(source);
let mut problem = self
.meta()
.into_details(env!("CARGO_PKG_NAME"), self.to_string());
problem.status = status;
problem.with_suggested_fix(fix).with_code_action(action)
},
Self::DocumentNotFound(_) => self
.meta()
.into_details(env!("CARGO_PKG_NAME"), self.to_string())
.with_suggested_fix(mif_problem::SuggestedFix::new(
"Ingest the document first with `mif-cli ingest`, or check the id for a \
typo.",
mif_problem::Applicability::MaybeIncorrect,
))
.with_code_action(mif_problem::CodeAction::new(
"Ingest the document before searching for similar ones",
"quickfix",
mif_problem::Applicability::MaybeIncorrect,
)),
Self::Json { .. } => self
.meta()
.into_details(env!("CARGO_PKG_NAME"), self.to_string()),
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
let format = OutputFormat::select(cli.format.as_deref(), std::io::stderr().is_terminal());
match run(&cli.command) {
Ok(message) => {
println!("{message}");
ExitCode::SUCCESS
},
Err(error) => {
eprintln!("{}", error.render(format));
ExitCode::from(error.to_problem().exit_code.unwrap_or(1))
},
}
}
fn run(command: &Command) -> Result<String, CliError> {
match command {
Command::Validate { file } => validate(file),
Command::Ontology { command } => match command {
OntologyCommand::Resolve { id, ontologies_dir } => resolve(id, ontologies_dir),
},
Command::Ingest { file, db_path } => ingest(file, db_path.as_deref()),
Command::Search {
query,
db_path,
limit,
} => search(query, db_path.as_deref(), *limit),
Command::FindSimilar { id, db_path, limit } => find_similar(id, db_path.as_deref(), *limit),
Command::CorpusStats { db_path } => corpus_stats(db_path.as_deref()),
}
}
fn resolve_db_path(db_path: Option<&Path>) -> PathBuf {
db_path.map_or_else(|| PathBuf::from(DEFAULT_DB_PATH), Path::to_path_buf)
}
fn format_matches(matches: &[mif_store::SimilarityMatch]) -> String {
if matches.is_empty() {
return "(no matches)".to_string();
}
matches
.iter()
.map(|m| format!("{:.4} {}", m.score, m.id))
.collect::<Vec<_>>()
.join("\n")
}
fn validate(file: &Path) -> Result<String, CliError> {
let contents = std::fs::read_to_string(file).map_err(|source| CliError::Io {
path: file.display().to_string(),
source,
})?;
let jsonld = project_to_jsonld(file, &contents)?;
mif_schema::validate_document(&jsonld)?;
Ok(format!("{}: valid", file.display()))
}
fn resolve(id: &str, ontologies_dir: &Path) -> Result<String, CliError> {
let corpus = mif_ontology::load_corpus_from_dir(ontologies_dir)?;
let chain = mif_ontology::resolve_chain(id, &corpus)?;
Ok(chain
.iter()
.map(|ontology| format!("{} ({})", ontology.id, ontology.version))
.collect::<Vec<_>>()
.join(" -> "))
}
fn project_to_jsonld(path: &Path, contents: &str) -> Result<serde_json::Value, CliError> {
let contents = contents.strip_prefix('\u{feff}').unwrap_or(contents);
if contents.trim_start().starts_with("---") {
mif_frontmatter::roundtrip_lossless(contents)?;
let (frontmatter, body) = mif_frontmatter::parse_markdown(contents)?;
Ok(mif_frontmatter::md_to_jsonld(&frontmatter, &body)?)
} else {
let jsonld: serde_json::Value =
serde_json::from_str(contents).map_err(|source| CliError::Json {
path: path.display().to_string(),
source,
})?;
let (frontmatter, body) =
mif_frontmatter::jsonld_to_md(&jsonld, mif_frontmatter::FrontmatterShape::V1Canonical)?;
let derived_md = mif_frontmatter::serialize_markdown(&frontmatter, &body)?;
mif_frontmatter::roundtrip_lossless(&derived_md)?;
Ok(jsonld)
}
}
fn content_hash(contents: &str) -> String {
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = FNV_OFFSET_BASIS;
for byte in contents.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
format!("{hash:016x}")
}
fn ingest(file: &Path, db_path: Option<&Path>) -> Result<String, CliError> {
let contents = std::fs::read_to_string(file).map_err(|source| CliError::Io {
path: file.display().to_string(),
source,
})?;
let jsonld = project_to_jsonld(file, &contents)?;
mif_schema::validate_document(&jsonld)?;
let id = jsonld
.get("@id")
.and_then(serde_json::Value::as_str)
.map_or_else(|| file.display().to_string(), ToString::to_string);
let content_text = jsonld
.get("content")
.and_then(serde_json::Value::as_str)
.unwrap_or(&contents);
let embedder = mif_embed::Embedder::load()?;
let vector = embedder.embed(content_text)?;
let db_path = db_path.map_or_else(|| PathBuf::from(DEFAULT_DB_PATH), Path::to_path_buf);
if let Some(parent) = db_path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent).map_err(|source| CliError::Io {
path: parent.display().to_string(),
source,
})?;
}
let store = mif_store::VectorStore::open(&db_path)?;
let hash = content_hash(&contents);
let updated_at = chrono::Utc::now().to_rfc3339();
store.upsert(&id, &vector, &hash, &updated_at)?;
Ok(format!(
"{}: lint=ok validate=ok roundtrip=lossless embedding_dim={} stored=true (id={id}, db={})",
file.display(),
vector.len(),
db_path.display()
))
}
fn search(query: &str, db_path: Option<&Path>, limit: usize) -> Result<String, CliError> {
let embedder = mif_embed::Embedder::load()?;
let vector = embedder.embed(query)?;
let db_path = resolve_db_path(db_path);
let store = mif_store::VectorStore::open(&db_path)?;
let matches = store.top_k_similar(&vector, limit)?;
Ok(format_matches(&matches))
}
fn find_similar(id: &str, db_path: Option<&Path>, limit: usize) -> Result<String, CliError> {
let db_path = resolve_db_path(db_path);
let store = mif_store::VectorStore::open(&db_path)?;
let anchor = store
.get(id)?
.ok_or_else(|| CliError::DocumentNotFound(id.to_string()))?;
let matches: Vec<_> = store
.top_k_similar(&anchor.vector, limit.saturating_add(1))?
.into_iter()
.filter(|m| m.id != id)
.take(limit)
.collect();
Ok(format_matches(&matches))
}
fn corpus_stats(db_path: Option<&Path>) -> Result<String, CliError> {
let db_path = resolve_db_path(db_path);
let store = mif_store::VectorStore::open(&db_path)?;
let stats = store.stats()?;
Ok(stats.dim.map_or_else(
|| format!("count=0 db={}", db_path.display()),
|dim| format!("count={} dim={dim} db={}", stats.count, db_path.display()),
))
}
#[cfg(test)]
mod tests {
use std::fs;
use mif_problem::{OutputFormat, ToProblem};
use super::{
CliError, Command, OntologyCommand, corpus_stats, find_similar, ingest, resolve, run,
search, validate,
};
fn write_temp_file(contents: &str) -> tempfile::NamedTempFile {
let file = tempfile::NamedTempFile::new().unwrap();
fs::write(file.path(), contents).unwrap();
file
}
fn warm_embedding_model_cache() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
let _ = mif_embed::Embedder::load();
});
}
#[test]
fn validate_accepts_a_conformant_document() {
let file = write_temp_file(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:test-001",
"conceptType": "semantic",
"content": "Test content.",
"created": "2026-07-02T00:00:00Z"
}"#,
);
assert_eq!(
validate(file.path()).unwrap(),
format!("{}: valid", file.path().display())
);
}
#[test]
fn validate_accepts_a_conformant_markdown_document() {
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
assert_eq!(
validate(file.path()).unwrap(),
format!("{}: valid", file.path().display())
);
}
#[test]
fn validate_accepts_a_markdown_document_with_a_leading_byte_order_mark() {
let file = write_temp_file(&format!("\u{feff}{VALID_MARKDOWN_FIXTURE}"));
assert_eq!(
validate(file.path()).unwrap(),
format!("{}: valid", file.path().display())
);
}
#[test]
fn validate_rejects_a_non_conformant_document() {
let file = write_temp_file(r#"{"content": "missing required fields"}"#);
let error = validate(file.path()).unwrap_err();
assert!(error.to_string().contains("failed schema validation"));
}
#[test]
fn validate_reports_invalid_json() {
let file = write_temp_file("not json");
let error = validate(file.path()).unwrap_err();
assert!(error.to_string().contains("failed to parse"));
}
#[test]
fn validate_reports_missing_file() {
let missing = std::path::Path::new("/nonexistent/mif-cli-test-fixture.json");
let error = validate(missing).unwrap_err();
assert!(error.to_string().contains("failed to read"));
}
#[test]
fn resolve_prints_the_extends_chain() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("mif-base.yaml"),
"ontology:\n id: mif-base\n version: 1.0.0\n",
)
.unwrap();
fs::write(
dir.path().join("domain.yaml"),
"ontology:\n id: domain\n version: 1.0.0\n extends: [mif-base]\n",
)
.unwrap();
assert_eq!(
resolve("domain", dir.path()).unwrap(),
"mif-base (1.0.0) -> domain (1.0.0)"
);
}
#[test]
fn resolve_reports_unknown_ontology() {
let dir = tempfile::tempdir().unwrap();
let error = resolve("missing", dir.path()).unwrap_err();
assert!(error.to_string().contains("not found"));
}
#[test]
fn invalid_document_error_renders_as_problem_json() {
let file = write_temp_file(r#"{"content": "missing required fields"}"#);
let error = validate(file.path()).unwrap_err();
let json = error.render(OutputFormat::Json);
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
for member in [
"type",
"title",
"status",
"detail",
"instance",
"retry_after",
] {
assert!(value.get(member).is_some(), "missing {member}");
}
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/invalid-document/v1"
);
}
#[test]
fn missing_file_io_error_classifies_as_a_404_maybe_incorrect_problem() {
let error =
validate(std::path::Path::new("/nonexistent/mif-cli-fixture.json")).unwrap_err();
let problem = error.to_problem();
assert_eq!(problem.status, 404);
assert_eq!(
problem.suggested_fix.unwrap().applicability,
mif_problem::Applicability::MaybeIncorrect
);
}
#[test]
fn directory_io_error_classifies_as_a_500_unspecified_problem() {
let dir = tempfile::tempdir().unwrap();
let error = validate(dir.path()).unwrap_err();
let problem = error.to_problem();
#[cfg(not(windows))]
{
assert_eq!(problem.status, 500);
assert_eq!(
problem.suggested_fix.unwrap().applicability,
mif_problem::Applicability::Unspecified
);
}
#[cfg(windows)]
{
assert_eq!(problem.status, 403);
assert_eq!(
problem.suggested_fix.unwrap().applicability,
mif_problem::Applicability::MaybeIncorrect
);
}
}
#[test]
fn unknown_ontology_error_renders_as_problem_json() {
let dir = tempfile::tempdir().unwrap();
let error = resolve("missing", dir.path()).unwrap_err();
let problem = error.to_problem();
assert_eq!(
problem.problem_type,
"https://modeled-information-format.github.io/mif-rs/references/errors/ontology-not-found/v1"
);
assert_eq!(problem.status, 404);
}
#[test]
fn pretty_render_matches_error_prefixed_display() {
let error =
validate(std::path::Path::new("/nonexistent/mif-cli-fixture.json")).unwrap_err();
assert_eq!(
error.render(OutputFormat::Pretty),
format!("Error: {error}")
);
}
const VALID_MARKDOWN_FIXTURE: &str = "---
id: memory:test-001
type: semantic
created: 2026-07-02T00:00:00Z
---
Test content.
";
#[test]
fn ingest_accepts_a_conformant_markdown_document_and_stores_it() {
warm_embedding_model_cache();
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let message = ingest(file.path(), Some(&db_path)).unwrap();
assert!(message.contains("lint=ok"));
assert!(message.contains("validate=ok"));
assert!(message.contains("roundtrip=lossless"));
assert!(message.contains("embedding_dim=384"));
assert!(message.contains("stored=true"));
let store = mif_store::VectorStore::open(&db_path).unwrap();
assert_eq!(store.count().unwrap(), 1);
let stored = store.get("urn:mif:memory:test-001").unwrap().unwrap();
assert_eq!(stored.dim, 384);
}
#[test]
fn ingest_accepts_a_conformant_jsonld_document() {
warm_embedding_model_cache();
let file = write_temp_file(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:test-002",
"conceptType": "semantic",
"content": "Other content.",
"created": "2026-07-02T00:00:00Z"
}"#,
);
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let message = ingest(file.path(), Some(&db_path)).unwrap();
assert!(message.contains("embedding_dim=384"));
let store = mif_store::VectorStore::open(&db_path).unwrap();
assert!(store.get("urn:mif:memory:test-002").unwrap().is_some());
}
#[test]
fn ingest_rejects_invalid_document_and_writes_no_row() {
warm_embedding_model_cache();
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let valid_file = write_temp_file(VALID_MARKDOWN_FIXTURE);
ingest(valid_file.path(), Some(&db_path)).unwrap();
let store = mif_store::VectorStore::open(&db_path).unwrap();
assert_eq!(store.count().unwrap(), 1);
let invalid_file = write_temp_file(
"---
id: memory:test-003
created: 2026-07-02T00:00:00Z
---
No type field.
",
);
let error = ingest(invalid_file.path(), Some(&db_path)).unwrap_err();
assert!(error.to_string().contains("failed schema validation"));
let store = mif_store::VectorStore::open(&db_path).unwrap();
assert_eq!(store.count().unwrap(), 1);
}
#[test]
fn ingest_invalid_document_renders_as_problem_json() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let invalid_file = write_temp_file(
"---
id: memory:test-004
created: 2026-07-02T00:00:00Z
---
No type field.
",
);
let error = ingest(invalid_file.path(), Some(&db_path)).unwrap_err();
let json = error.render(OutputFormat::Json);
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
for member in [
"type",
"title",
"status",
"detail",
"instance",
"retry_after",
"suggested_fix",
"code_actions",
] {
assert!(value.get(member).is_some(), "missing {member}");
}
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/invalid-document/v1"
);
}
#[test]
fn ingest_missing_file_classifies_as_a_404_maybe_incorrect_problem() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let error = ingest(
std::path::Path::new("/nonexistent/mif-cli-fixture.json"),
Some(&db_path),
)
.unwrap_err();
assert_eq!(error.to_problem().status, 404);
}
#[test]
fn ingest_reports_an_io_error_when_the_db_parent_directory_cannot_be_created() {
warm_embedding_model_cache();
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let parent_dir = tempfile::tempdir().unwrap();
let blocker = parent_dir.path().join("blocker");
fs::write(&blocker, "not a directory").unwrap();
let db_path = blocker.join("subdir").join("vectors.db");
let error = ingest(file.path(), Some(&db_path)).unwrap_err();
assert_eq!(error.to_problem().status, 500);
}
#[test]
fn ingest_reports_the_real_file_path_on_a_json_ld_parse_error() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let file = write_temp_file("not valid json");
let error = ingest(file.path(), Some(&db_path)).unwrap_err();
let message = error.to_string();
assert!(
message.contains(&file.path().display().to_string()),
"expected the real file path in {message:?}, not the ingest-input placeholder"
);
}
#[test]
fn exit_code_reflects_the_mapped_problem_type_not_a_flat_failure() {
let io_error = CliError::Io {
path: "x".to_string(),
source: std::io::Error::from(std::io::ErrorKind::NotFound),
};
assert_eq!(io_error.to_problem().exit_code, Some(1));
let json_error = CliError::Json {
path: "x".to_string(),
source: serde_json::from_str::<serde_json::Value>("not json").unwrap_err(),
};
assert_eq!(json_error.to_problem().exit_code, Some(2));
let not_found = CliError::DocumentNotFound("urn:mif:memory:missing".to_string());
assert_eq!(not_found.to_problem().exit_code, Some(3));
}
#[test]
fn delegated_error_variants_render_a_sane_problem_if_ever_directly_matched() {
for error in [
CliError::Frontmatter(mif_frontmatter::FrontmatterError::MissingFrontmatter),
CliError::Embed(mif_embed::EmbedError::NoCacheDir { model: "test" }),
CliError::Store(mif_store::StoreError::MissingParentDir {
path: "test".to_string(),
}),
] {
let problem = error.to_problem();
assert!(problem.status >= 400, "status was {}", problem.status);
let meta = error.meta();
assert_eq!(meta.status, 500);
}
}
#[test]
fn run_dispatches_every_subcommand_to_its_handler() {
let ontologies_dir = tempfile::tempdir().unwrap();
fs::write(
ontologies_dir.path().join("base.yaml"),
"ontology:\n id: base\n version: 1.0.0\n",
)
.unwrap();
let resolve_result = run(&Command::Ontology {
command: OntologyCommand::Resolve {
id: "base".to_string(),
ontologies_dir: ontologies_dir.path().to_path_buf(),
},
});
assert!(resolve_result.is_ok());
let missing_file = tempfile::tempdir().unwrap().path().join("missing.json");
let validate_result = run(&Command::Validate { file: missing_file });
assert!(validate_result.is_err());
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
warm_embedding_model_cache();
let doc_file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let ingest_result = run(&Command::Ingest {
file: doc_file.path().to_path_buf(),
db_path: Some(db_path.clone()),
});
assert!(ingest_result.is_ok());
let search_result = run(&Command::Search {
query: "test content".to_string(),
db_path: Some(db_path.clone()),
limit: 5,
});
assert!(search_result.is_ok());
let find_similar_result = run(&Command::FindSimilar {
id: "urn:mif:memory:test-001".to_string(),
db_path: Some(db_path.clone()),
limit: 5,
});
assert!(find_similar_result.is_ok());
let corpus_stats_result = run(&Command::CorpusStats {
db_path: Some(db_path),
});
assert!(corpus_stats_result.is_ok());
}
fn ingest_fixture(db_path: &std::path::Path, id: &str, content: &str) {
warm_embedding_model_cache();
let file = write_temp_file(&format!(
"---\nid: {id}\ntype: semantic\ncreated: 2026-07-02T00:00:00Z\n---\n\n{content}\n"
));
ingest(file.path(), Some(db_path)).unwrap();
}
#[test]
fn search_ranks_ingested_documents_by_relevance() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
ingest_fixture(
&db_path,
"memory:cats",
"Cats are small domesticated felines.",
);
ingest_fixture(
&db_path,
"memory:finance",
"Quarterly revenue exceeded analyst expectations.",
);
let result = search("A furry pet cat", Some(&db_path), 10).unwrap();
let first_line = result.lines().next().unwrap();
assert!(first_line.ends_with("urn:mif:memory:cats"));
}
#[test]
fn search_reports_no_matches_on_an_empty_store() {
warm_embedding_model_cache();
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
mif_store::VectorStore::open(&db_path).unwrap();
let result = search("anything", Some(&db_path), 10).unwrap();
assert_eq!(result, "(no matches)");
}
#[test]
fn find_similar_excludes_the_anchor_document_itself() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
ingest_fixture(&db_path, "memory:a", "Cats are small domesticated felines.");
ingest_fixture(&db_path, "memory:b", "Dogs are loyal domesticated canines.");
let result = find_similar("urn:mif:memory:a", Some(&db_path), 10).unwrap();
assert!(!result.contains("memory:a"));
assert!(result.contains("memory:b"));
}
#[test]
fn find_similar_reports_document_not_found() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
mif_store::VectorStore::open(&db_path).unwrap();
let error = find_similar("urn:mif:memory:missing", Some(&db_path), 10).unwrap_err();
let problem = error.to_problem();
assert_eq!(
problem.problem_type,
"https://modeled-information-format.github.io/mif-rs/references/errors/mif-cli-document-not-found/v1"
);
assert_eq!(problem.status, 404);
}
#[test]
fn corpus_stats_reports_count_and_dim() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
assert_eq!(
corpus_stats(Some(&db_path)).unwrap(),
format!("count=0 db={}", db_path.display())
);
ingest_fixture(&db_path, "memory:one", "Some content.");
let result = corpus_stats(Some(&db_path)).unwrap();
assert!(result.contains("count=1"));
assert!(result.contains("dim=384"));
}
}