nomograph-workflow 0.2.0

Shared workflow layer for nomograph claim-substrate tools: Store adapter, phase state machine, v1 legacy detection, cross-tool helpers.
Documentation
//! v1 → v2 legacy detection.
//!
//! Synthesist v1 stored its SQLite database at `.synth/main.db`. v2
//! stores a claim log at `claims/`. If a user runs a v2 tool in a
//! directory that still has `.synth/main.db` but no `claims/`, we
//! refuse to silently create an empty v2 estate alongside the legacy
//! data — that's the path that orphans the user's work.
//!
//! Instead, every v2 tool's `Store::discover` path calls
//! [`find_legacy_v1_db`] first and bails with
//! [`legacy_migration_error`] — a prescriptive message naming the
//! exact migrator command to run.

use std::path::{Path, PathBuf};

/// Walk upward from `start` looking for a v1 `.synth/main.db`.
/// Returns the path to the first one found, or `None`.
///
/// Used by v2 tools (synthesist, lattice) inside their discover path
/// so a bad directory produces a prescriptive error instead of a
/// silent empty-estate creation.
pub fn find_legacy_v1_db(start: &Path) -> Option<PathBuf> {
    let mut cur = start.to_path_buf();
    loop {
        let legacy = cur.join(".synth").join("main.db");
        if legacy.is_file() {
            return Some(legacy);
        }
        if !cur.pop() {
            break;
        }
    }
    None
}

/// Build the standardized migration error. Centralized so every
/// caller emits an identical prescriptive message. The message names
/// the exact `synthesist migrate v1-to-v2` subcommand to run and
/// points at `MIGRATION.md` for rollback + verification steps.
pub fn legacy_migration_error(legacy_db: &Path) -> anyhow::Error {
    // legacy_db = <repo>/.synth/main.db. Compute <repo>/claims as the
    // suggested migration target.
    let target = legacy_db
        .parent()
        .and_then(|p| p.parent())
        .map(|p| p.join("claims"))
        .unwrap_or_else(|| PathBuf::from("claims"));
    anyhow::anyhow!(
        "found v1 database at `{legacy}` but no v2 `claims/` directory in scope.\n\
         \n\
         synthesist v2 stores data in `claims/` (Automerge claim log + SQLite view).\n\
         Port your v1 data before using v2 commands here:\n\
         \n\
         \u{20}\u{20}# dry-run first, then migrate\n\
         \u{20}\u{20}synthesist migrate v1-to-v2 --from {legacy} --to {target} --dry-run\n\
         \u{20}\u{20}synthesist migrate v1-to-v2 --from {legacy} --to {target}\n\
         \n\
         See MIGRATION.md in the synthesist repo for rollback + verification.\n\
         If the v1 db is obsolete and you want a fresh v2 estate here,\n\
         remove `.synth/` first.",
        legacy = legacy_db.display(),
        target = target.display(),
    )
}

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

    #[test]
    fn find_legacy_returns_none_for_clean_dir() {
        let tmp = tempfile::tempdir().unwrap();
        assert!(find_legacy_v1_db(tmp.path()).is_none());
    }

    #[test]
    fn find_legacy_walks_upward() {
        let tmp = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(tmp.path().join(".synth")).unwrap();
        std::fs::write(tmp.path().join(".synth/main.db"), b"fake").unwrap();
        let nested = tmp.path().join("a/b/c");
        std::fs::create_dir_all(&nested).unwrap();
        let found = find_legacy_v1_db(&nested).expect("walks up");
        assert!(found.ends_with(".synth/main.db"));
    }

    #[test]
    fn legacy_error_names_target_dir() {
        let tmp = tempfile::tempdir().unwrap();
        let db = tmp.path().join(".synth/main.db");
        let err = legacy_migration_error(&db).to_string();
        assert!(err.contains("v1 database"));
        assert!(err.contains("synthesist migrate v1-to-v2"));
        assert!(err.contains("MIGRATION.md"));
        // Target path = tmp/claims.
        assert!(err.contains(&tmp.path().join("claims").display().to_string()));
    }
}