use std::fs;
use aristo_core::canon::cache::CanonMatchesFile;
use aristo_core::canon::{
AnnotationMatchInput, CanonClient, CanonError, CanonMatchRequest, HttpCanonClient,
MockCanonClient,
};
use aristo_core::index::{BindingState, IdNamespace, IndexEntry, IndexFile};
use crate::commands::index::workspace_or_error;
use crate::{CliError, CliResult};
pub(crate) fn run() -> CliResult<()> {
let ws = workspace_or_error()?;
let index_path = ws.index_path();
if !index_path.is_file() {
return Err(CliError::Other {
message: format!(
"no .aristo/index.toml at {} — run `aristo stamp` first",
index_path.display()
),
exit_code: 2,
});
}
let index_raw = fs::read_to_string(&index_path).map_err(CliError::Io)?;
let index: IndexFile = toml::from_str(&index_raw).map_err(|e| CliError::Other {
message: format!("parsing {}: {e}", index_path.display()),
exit_code: 1,
})?;
let bound: Vec<&aristo_core::index::IntentEntry> = index
.entries
.iter()
.filter_map(|(id, entry)| {
let ns = id.namespace();
if !matches!(ns, IdNamespace::Aristos | IdNamespace::Kanon) {
return None;
}
if let IndexEntry::Intent(e) = entry {
if matches!(e.binding, BindingState::Bound { .. }) {
return Some(e);
}
}
None
})
.collect();
let bound_ids: Vec<&aristo_core::index::AnnotationId> = index
.entries
.iter()
.filter(|(id, entry)| {
let ns = id.namespace();
if !matches!(ns, IdNamespace::Aristos | IdNamespace::Kanon) {
return false;
}
if let IndexEntry::Intent(e) = entry {
matches!(e.binding, BindingState::Bound { .. })
} else {
false
}
})
.map(|(id, _)| id)
.collect();
if bound.is_empty() {
println!("ok: no canon-bound annotations in the index. Nothing to migrate.");
return Ok(());
}
let cache_path = ws.canon_matches_path();
let cache = CanonMatchesFile::read(&cache_path).map_err(CliError::Io)?;
let client: Box<dyn CanonClient> = if let Some(mock) = MockCanonClient::from_env() {
Box::new(mock)
} else {
match aristo_core::auth::resolve_full() {
Ok(creds) => {
let base_url = std::env::var("ARETTA_API_URL")
.unwrap_or_else(|_| creds.server.as_str().to_string());
Box::new(HttpCanonClient::new(base_url, &creds.token))
}
Err(_) => {
return Err(CliError::Other {
message: "canon API requires authentication.\n \
Run `aristo auth login` first."
.into(),
exit_code: 1,
});
}
}
};
let request = CanonMatchRequest {
annotations: bound
.iter()
.map(|e| AnnotationMatchInput {
annotation_text: e.text.clone(),
applies_to: applies_to_from_site(&e.site),
})
.collect(),
confidence_threshold: 0.65,
};
let response = client
.match_annotations(&request)
.map_err(canon_error_to_cli)?;
println!();
println!("canon migration report:");
println!();
let mut current = 0usize;
let mut patch_bump = 0usize;
let mut minor_bump = 0usize;
for (i, prefixed_id) in bound_ids.iter().enumerate() {
let cached = cache
.entries
.get(prefixed_id)
.and_then(|e| e.accepted_matches.first());
let Some(cached_match) = cached else {
println!(
" {prefixed_id}: ? (no cached accepted_match — re-stamp to refresh)",
prefixed_id = prefixed_id.as_str()
);
continue;
};
let candidates = response.results.get(i).cloned().unwrap_or_default();
let same_canon = candidates
.iter()
.find(|c| c.canon_id == cached_match.canon_id);
match same_canon {
Some(c) if c.version == cached_match.version => {
current += 1;
println!(
" {prefixed_id}: current ({}@{})",
cached_match.canon_id, cached_match.version,
);
}
Some(c) => {
patch_bump += 1;
println!(
" {prefixed_id}: patch-bump ({} {} → {}). Run `aristo canon refresh` to update the cache.",
cached_match.canon_id, cached_match.version, c.version,
);
}
None => {
minor_bump += 1;
println!(
" {prefixed_id}: minor-bump ({} retired from catalog). Run `aristo canon unbind {prefixed_id}` then re-stamp.",
cached_match.canon_id,
prefixed_id = prefixed_id.as_str(),
);
}
}
}
println!();
println!("totals: {current} current, {patch_bump} patch-bump, {minor_bump} minor-bump");
Ok(())
}
fn applies_to_from_site(site: &str) -> Vec<String> {
let head = site.split_whitespace().next().unwrap_or("");
let kw = match head {
"fn" => "fn",
"struct" => "struct",
"enum" => "enum",
"trait" => "trait",
"impl" => "method",
"mod" => "mod",
"type" => "type",
_ => return vec![],
};
vec![kw.to_string()]
}
fn canon_error_to_cli(e: CanonError) -> CliError {
CliError::Other {
message: format!("canon API error: {e}"),
exit_code: 1,
}
}