sirno 0.0.5

Sirno gives project design a semantic intermediate representation.
Documentation
//! Error types for command execution and presentation.

use std::ffi::OsString;
use std::fmt;
use std::path::PathBuf;

use thiserror::Error;

use crate::{
    ConfigError, EntryAddress, EntryAddressError, EntryArtifactPathError, EntryAtomError,
    EntryDirectoryError, EntryParseError, FrostError, GeneratedLinkError, LockError, TideError,
    TutorialSettings, UpstreamError, WitnessError,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OpenTideTutorial {
    pub(crate) frost_commit_tide: bool,
    pub(crate) frost_bootstrap_tide: bool,
    pub(crate) bootstrap: bool,
}

impl OpenTideTutorial {
    pub(crate) fn new(settings: Option<TutorialSettings>, bootstrap: bool) -> Self {
        let Some(settings) = settings else {
            return Self { frost_commit_tide: false, frost_bootstrap_tide: false, bootstrap };
        };
        Self {
            frost_commit_tide: settings.frost_commit_tide,
            frost_bootstrap_tide: settings.frost_bootstrap_tide,
            bootstrap,
        }
    }
}

impl fmt::Display for OpenTideTutorial {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        if !self.frost_commit_tide {
            return Ok(());
        }

        writeln!(formatter)?;
        writeln!(formatter)?;
        writeln!(formatter, "Tutorial:")?;
        writeln!(
            formatter,
            "A tide is the review worklist for differences between the waterline and frostline.",
        )?;
        if self.bootstrap && self.frost_bootstrap_tide {
            writeln!(
                formatter,
                "This frost path is still at empty version 0, so the first commit compares",
            )?;
            writeln!(formatter, "the full lake to an empty frostline.")?;
        }
        writeln!(formatter, "Inspect the work with `sirno tide status`.")?;
        writeln!(formatter, "Resolve reviewed work with `sirno resolve ...`,",)?;
        writeln!(
            formatter,
            "or choose the current lake as the baseline with `sirno commit --unsafe-resolve-all`.",
        )?;
        write!(formatter, "Remove `[tutorial]` from Sirno.toml, or set tutorial knobs to false,",)?;
        write!(formatter, " to silence tutorial text.")
    }
}

/// Error raised while running the CLI.
#[derive(Debug, Error)]
pub enum CommandError {
    /// Sirno Frost has already been configured at another path.
    #[error("frost is already configured at {0}")]
    FrostAlreadyConfigured(PathBuf),
    /// Sirno Frost is required for a frost command but is not configured.
    #[error("frost is not configured; run `sirno frost init` first")]
    FrostNotConfigured,
    /// Immutable frost checkouts cannot be committed.
    #[error("frost version {0} is checked out immutably; use checkout --unsafe-mutable first")]
    ImmutableFrostCheckout(u64),
    /// Frost commit requires all tide workitems to be resolved.
    #[error("tide has {count} open workitems; run `sirno tide status`{tutorial}")]
    OpenTide {
        /// Number of open tide workitems.
        count: usize,
        /// Optional tutorial text controlled by Sirno.toml.
        tutorial: OpenTideTutorial,
    },
    /// Empty frost cannot be checked out as a version.
    #[error("frost version {0} is not a check-outable snapshot")]
    InvalidFrostVersion(u64),
    /// Frost checkout needs one target selector.
    #[error("frost checkout requires `latest` or `version`")]
    MissingFrostCheckoutTarget,
    /// Frost garbage collection must preserve the current editable frostline.
    #[error("frost gc requires the current mutable lake; version {0} is checked out")]
    FrostGcRequiresCurrentLake(u64),
    /// An artifact source path did not have a file name for the default artifact path.
    #[error("artifact source has no file name: {0}")]
    ArtifactSourceHasNoFileName(PathBuf),
    /// A configured path move cannot replace an existing destination.
    #[error("move destination already exists: {0}")]
    MoveDestinationExists(PathBuf),
    /// A configured path move could not inspect its destination.
    #[error("failed to inspect move destination {path}")]
    ReadMoveDestination {
        /// Destination path that could not be inspected.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A configured path move could not prepare a temporary staging path.
    #[error("failed to prepare move staging path near {path}")]
    PrepareMoveStagingPath {
        /// Directory where the staging path would be created.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A configured path move could not create the destination parent.
    #[error("failed to create move destination parent {path}")]
    CreateMoveDestinationParent {
        /// Destination parent path.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A configured path could not be moved.
    #[error("failed to move {source_path} to {destination_path}")]
    MovePath {
        /// Source path configured before the move.
        source_path: PathBuf,
        /// Destination path configured by the move.
        destination_path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A configured path move failed after staging and could not restore the source path.
    #[error(
        "failed to move {source_path} to {destination_path}; rollback from {staging_path} failed: {rollback}"
    )]
    MovePathRollback {
        /// Source path configured before the move.
        source_path: PathBuf,
        /// Destination path configured by the move.
        destination_path: PathBuf,
        /// Temporary staging path that still holds the moved directory.
        staging_path: PathBuf,
        /// Underlying move error.
        #[source]
        source: std::io::Error,
        /// Rollback rename error.
        rollback: std::io::Error,
    },
    /// A config write failed after a configured path was moved, and the rollback also failed.
    #[error(
        "failed to write config after moving {source_path} to {destination_path}; rollback failed: {rollback}"
    )]
    MoveConfigWriteRollback {
        /// Source path configured before the move.
        source_path: PathBuf,
        /// Destination path already moved into place.
        destination_path: PathBuf,
        /// Config write error.
        #[source]
        source: Box<ConfigError>,
        /// Rollback rename error.
        rollback: std::io::Error,
    },
    /// Witness lookup requires configured repo members.
    #[error("repo members are not configured; add [repo].members to Sirno.toml")]
    RepoMembersNotConfigured,
    /// Witness lookup requires an existing entry address.
    #[error("entry `{0}` does not exist")]
    MissingWitnessEntry(EntryAddress),
    /// Lake path override does not apply to checking a frost path directly.
    #[error("`--lake-path` cannot be used with `check --frost-path`")]
    LakePathWithFrostPath,
    /// Frost path override applies only to direct frost checks.
    #[error("`--frost-path` only applies to `sirno check`")]
    FrostPathRequiresCheck,
    /// The MCP server selects its project only through the config path.
    #[error("`--lake-path` cannot be used with `sirno util mcp`; configure the lake in Sirno.toml")]
    McpRejectsLakePath,
    /// The MCP server selects its project only through the config path.
    #[error("`--frost-path` cannot be used with `sirno util mcp`; use `--config` only")]
    McpRejectsFrostPath,
    /// The config utility only inspects the config file.
    #[error("`--lake-path` cannot be used with `sirno util config`; use `--config` only")]
    ConfigRejectsLakePath,
    /// The config utility only inspects the config file.
    #[error("`--frost-path` cannot be used with `sirno util config`; use `--config` only")]
    ConfigRejectsFrostPath,
    /// The terminal UI failed.
    #[error("terminal UI failed")]
    TerminalUi(#[source] std::io::Error),
    /// The interactive init prompt failed while reading or writing the terminal.
    #[error("interactive init prompt failed")]
    InteractiveInit(#[source] std::io::Error),
    /// The interactive init prompt reached the end of its input.
    #[error("interactive init prompt reached end of input")]
    InteractiveInitEof,
    /// The async MCP runtime could not be created.
    #[error("failed to create MCP runtime")]
    CreateMcpRuntime(#[source] std::io::Error),
    /// The MCP server failed.
    #[error("MCP server failed: {0}")]
    McpServer(String),
    /// The skill wrapper utility uses bundled wrapper constants.
    #[error("`--lake-path` cannot be used with `sirno util skills`; wrappers are bundled")]
    SkillsRejectsLakePath,
    /// A skill wrapper package target could not be read.
    #[error("failed to read skill wrapper target {path}")]
    ReadSkillWrapperTarget {
        /// Target path that could not be read.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A skill wrapper package directory could not be created.
    #[error("failed to create skill wrapper target directory {path}")]
    CreateSkillWrapperTargetDirectory {
        /// Target directory that could not be created.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A skill wrapper package target could not be written.
    #[error("failed to write skill wrapper target {path}")]
    WriteSkillWrapperTarget {
        /// Target path that could not be written.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A skill wrapper link target already exists as a non-link filesystem object.
    #[error("skill wrapper target exists and is not a symlink: {0}")]
    SkillWrapperTargetExists(PathBuf),
    /// A stale skill wrapper symlink could not be removed.
    #[error("failed to remove skill wrapper target {path}")]
    RemoveSkillWrapperTarget {
        /// Target path that could not be removed.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A skill wrapper package target could not be linked.
    #[error("failed to link skill wrapper target {target_path} to {source_path}")]
    LinkSkillWrapperTarget {
        /// Link source path.
        source_path: PathBuf,
        /// Link target path.
        target_path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// Dry-run mode applies only to render writing.
    #[error("`--dry` only applies to `sirno render` without a subcommand")]
    DryWithRenderSubcommand,
    /// Render JSON overrides apply only to generated-link writing.
    #[error("`--override-json` only applies to `sirno render` without a subcommand")]
    OverrideJsonWithRenderSubcommand,
    /// A command named a structural field not configured for this project.
    #[error("structural field `{0}` is not configured; add [structural.{0}] to Sirno.toml")]
    UnconfiguredStructuralField(String),
    /// Generated-footer masking cannot compose with another ripgrep preprocessor.
    #[error(
        "generated-footer filtering cannot be combined with `rg --pre`; use `--with-generated-footer`"
    )]
    RgPreprocessorConflict,
    /// Ripgrep generated-footer preprocessor received an unexpected argument shape.
    #[error("rg generated-footer preprocessor expects one path argument")]
    RgPreprocessorArgumentCount,
    /// The current executable path could not be resolved.
    #[error("failed to locate current executable for rg preprocessor")]
    LocateCurrentExe(#[source] std::io::Error),
    /// The process current working directory could not be changed.
    #[error("failed to change current working directory to {path}")]
    ChangeCurrentDirectory {
        /// Target working directory.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// The current working directory could not be resolved.
    #[error("failed to locate current working directory")]
    CurrentDirectory(#[source] std::io::Error),
    /// A temporary ripgrep preprocessor invoker could not be created.
    #[error("failed to create rg preprocessor invoker at {path}")]
    CreateRgPreprocessorInvoker {
        /// Invoker path that could not be created.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// The generated-footer preprocessor could not read one file.
    #[error("failed to read rg preprocessor input {path}")]
    ReadRgPreprocessorInput {
        /// Path passed by ripgrep.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// The generated-footer preprocessor could not write masked output.
    #[error("failed to write rg preprocessor output")]
    WriteRgPreprocessorOutput(#[source] std::io::Error),
    /// Config-backed command failed.
    #[error(transparent)]
    Config(#[from] ConfigError),
    /// Lock-backed command failed.
    #[error(transparent)]
    Lock(#[from] LockError),
    /// Sirno-Frost-backed command failed.
    #[error(transparent)]
    Frost(#[from] FrostError),
    /// Witness lookup failed.
    #[error(transparent)]
    Witness(#[from] WitnessError),
    /// Sirno Lake entry directory command failed.
    #[error(transparent)]
    EntryDirectory(#[from] EntryDirectoryError),
    /// Entry address parsing failed.
    #[error(transparent)]
    EntryAddress(#[from] EntryAddressError),
    /// Entry atom parsing failed.
    #[error(transparent)]
    EntryAtom(#[from] EntryAtomError),
    /// Entry artifact path parsing failed.
    #[error(transparent)]
    ArtifactPath(#[from] EntryArtifactPathError),
    /// Entry metadata construction failed.
    #[error(transparent)]
    EntryParse(#[from] EntryParseError),
    /// Generated-link footer handling failed.
    #[error(transparent)]
    GeneratedLink(#[from] GeneratedLinkError),
    /// Tide operation failed.
    #[error(transparent)]
    Tide(#[from] TideError),
    /// Upstream operation failed.
    #[error(transparent)]
    Upstream(#[from] UpstreamError),
    /// Ripgrep could not be started.
    #[error("failed to run rg")]
    RunRg(#[source] std::io::Error),
    /// JSON-oriented ripgrep execution needs UTF-8 arguments.
    #[error("rg argument is not valid UTF-8: {0:?}")]
    RgArgumentNotUtf8(OsString),
    /// JSON parsing or rendering failed.
    #[error(transparent)]
    Json(#[from] serde_json::Error),
}