memlay 0.1.2

Repo-native, conflict-resistant shared memory and codebase navigation layer for AI coding agents
use std::fmt;

/// Stable top-level error codes exposed in `--json` output and MCP errors (PRD ยง23).
/// Some variants are reserved by the PRD contract for paths not yet reachable
/// in this build (P1 archive workflow, index busy signaling).
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
    NotAGitRepository,
    NotInitialized,
    InvalidConfig,
    InvalidRecord,
    ImmutableRecordChanged,
    SemanticConflict,
    IndexCorrupt,
    IndexBusy,
    PathOutsideRepository,
    McpProtocolError,
    AuditWriteFailed,
    TeamMemoryBehind,
    TeamMemoryUnknown,
    ChangeRecordRequired,
    DuplicateKeyConfirmationRequired,
    KeyAliasConflict,
    KeyAliasCycle,
    BackfillDraftInvalid,
    ArchivePlanMismatch,
    ArchiveRefMissing,
    P1NotImplemented,
    Internal,
}

impl ErrorCode {
    pub fn as_str(self) -> &'static str {
        match self {
            ErrorCode::NotAGitRepository => "NOT_A_GIT_REPOSITORY",
            ErrorCode::NotInitialized => "NOT_INITIALIZED",
            ErrorCode::InvalidConfig => "INVALID_CONFIG",
            ErrorCode::InvalidRecord => "INVALID_RECORD",
            ErrorCode::ImmutableRecordChanged => "IMMUTABLE_RECORD_CHANGED",
            ErrorCode::SemanticConflict => "SEMANTIC_CONFLICT",
            ErrorCode::IndexCorrupt => "INDEX_CORRUPT",
            ErrorCode::IndexBusy => "INDEX_BUSY",
            ErrorCode::PathOutsideRepository => "PATH_OUTSIDE_REPOSITORY",
            ErrorCode::McpProtocolError => "MCP_PROTOCOL_ERROR",
            ErrorCode::AuditWriteFailed => "AUDIT_WRITE_FAILED",
            ErrorCode::TeamMemoryBehind => "TEAM_MEMORY_BEHIND",
            ErrorCode::TeamMemoryUnknown => "TEAM_MEMORY_UNKNOWN",
            ErrorCode::ChangeRecordRequired => "CHANGE_RECORD_REQUIRED",
            ErrorCode::DuplicateKeyConfirmationRequired => "DUPLICATE_KEY_CONFIRMATION_REQUIRED",
            ErrorCode::KeyAliasConflict => "KEY_ALIAS_CONFLICT",
            ErrorCode::KeyAliasCycle => "KEY_ALIAS_CYCLE",
            ErrorCode::BackfillDraftInvalid => "BACKFILL_DRAFT_INVALID",
            ErrorCode::ArchivePlanMismatch => "ARCHIVE_PLAN_MISMATCH",
            ErrorCode::ArchiveRefMissing => "ARCHIVE_REF_MISSING",
            ErrorCode::P1NotImplemented => "P1_NOT_IMPLEMENTED",
            ErrorCode::Internal => "INTERNAL",
        }
    }
}

impl fmt::Display for ErrorCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, thiserror::Error)]
#[error("{code}: {message}")]
pub struct MemlayError {
    pub code: ErrorCode,
    pub message: String,
}

impl MemlayError {
    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
        Self {
            code,
            message: message.into(),
        }
    }
}

pub fn err(code: ErrorCode, message: impl Into<String>) -> anyhow::Error {
    anyhow::Error::new(MemlayError::new(code, message))
}

/// Extract the stable error code from an anyhow chain, defaulting to INTERNAL.
pub fn code_of(e: &anyhow::Error) -> ErrorCode {
    e.downcast_ref::<MemlayError>()
        .map(|m| m.code)
        .unwrap_or(ErrorCode::Internal)
}