use super::{diagnose, DiagnosisReport, Strategy, TargetContract};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrateOptions {
pub store: Option<PathBuf>,
pub destination: Option<PathBuf>,
pub scratch: Option<PathBuf>,
pub strategy: Strategy,
pub dry_run: bool,
}
impl Default for MigrateOptions {
fn default() -> Self {
Self {
store: None,
destination: None,
scratch: None,
strategy: Strategy::Auto,
dry_run: false,
}
}
}
const MIGRATION_COMPLETE: &str =
"the migration is complete: the rebuilt store now sits at the source's \
path, its provenance stamp names the target embedder, and the archived \
old store has been freed. RESTART the daemon — it holds its store as a \
handle taken at startup, and until it restarts it keeps serving the old \
data from memory. The .migration-journal directory beside the (now \
empty) destination path is the journal of what happened; it may be \
removed once you no longer want the evidence.";
pub fn parse(args: &[String]) -> Result<MigrateOptions, String> {
let mut options = MigrateOptions::default();
let mut index = 0;
while index < args.len() {
let flag = args[index].as_str();
if flag == "--dry-run" {
options.dry_run = true;
index += 1;
continue;
}
let value = value_for(args, index, flag)?;
apply_valued_flag(&mut options, flag, value)?;
index += 2;
}
Ok(options)
}
fn value_for<'a>(args: &'a [String], index: usize, flag: &str) -> Result<&'a String, String> {
args.get(index + 1)
.ok_or_else(|| format!("{flag} requires a value"))
}
fn apply_valued_flag(
options: &mut MigrateOptions,
flag: &str,
value: &String,
) -> Result<(), String> {
match flag {
"--store" => options.store = Some(PathBuf::from(value)),
"--destination" => options.destination = Some(PathBuf::from(value)),
"--scratch" => options.scratch = Some(PathBuf::from(value)),
"--strategy" => options.strategy = Strategy::parse(value)?,
other => return Err(format!("unknown migrate-embeddings flag {other:?}")),
}
Ok(())
}
pub fn dry_run(
store: &Path,
scratch_parent: &Path,
target: &TargetContract,
destination: Option<&Path>,
) -> Result<DiagnosisReport, crate::MemoryError> {
diagnose(store, scratch_parent, target, destination)
}
pub fn require_destination(options: &MigrateOptions) -> Result<PathBuf, String> {
options.destination.clone().ok_or_else(|| {
"a non-dry-run migrate-embeddings rebuilds into a destination you name: \
pass --destination <dir> (an empty or not-yet-existing directory on \
the store's filesystem), or --dry-run to only diagnose"
.to_owned()
})
}
#[must_use]
pub fn render(report: &DiagnosisReport) -> String {
let guidance = report
.resolution
.guidance()
.map_or_else(String::new, |next| format!("{next}\n\n"));
format!(
"{}\n\n{guidance}{}{}{}",
report.resolution.diagnostic(),
render_identity(report),
render_inventory(report),
render_blockers(report),
)
}
fn render_identity(report: &DiagnosisReport) -> String {
let provenance = match &report.source_provenance {
super::SourceProvenance::Known { model, dimension } => {
format!("{model} ({dimension} dimensions)")
}
super::SourceProvenance::Unknown { .. } => {
"unknown — not inferred from the width".to_owned()
}
};
let source_dimension = report
.source_dimension
.map_or_else(|| "no shared width".to_owned(), |d| d.to_string());
format!(
" store: {}\n \
source provenance: {provenance}\n \
source dimension: {source_dimension}\n \
target model: {} ({} dimensions)\n \
requested strategy: {:?}\n \
report format: v{}\n\n",
report.source_path.display(),
report.target_model,
report.target_dimension,
report.requested_strategy,
report.format_version,
)
}
fn render_inventory(report: &DiagnosisReport) -> String {
format!(
" facts: {}\n \
edges: {}\n \
working contexts: {}\n \
facts with a TTL: {}\n \
bytes on disk: {}\n\n",
report.facts,
report.edges,
report.working_contexts,
report.ttl_summary.with_expiry,
report.bytes_on_disk,
)
}
fn render_blockers(report: &DiagnosisReport) -> String {
if report.blockers.is_empty() {
return "no outstanding blockers.\n".to_owned();
}
let listed = report
.blockers
.iter()
.fold(String::new(), |mut acc, blocker| {
acc.push_str(" - ");
acc.push_str(blocker);
acc.push('\n');
acc
});
format!(
"{} blocker(s) before a rebuild:\n{listed}",
report.blockers.len()
)
}
#[must_use]
pub fn refuses(report: &DiagnosisReport) -> bool {
!report.resolution.runs()
}
#[must_use]
pub fn migration_complete_notice() -> &'static str {
MIGRATION_COMPLETE
}
pub fn default_scratch_parent(store: &Path) -> Result<PathBuf, String> {
match store.parent() {
Some(parent) if !parent.as_os_str().is_empty() => Ok(parent.to_path_buf()),
_ => Err(format!(
"cannot derive a scratch parent beside {}: pass --scratch <dir>. The diagnosis \
copies the whole store there, so a directory on the store's own volume is best",
store.display()
)),
}
}