use prikk_error::{PrikkError, Result};
use crate::layout::RepositoryLayout;
use crate::refs::{RefRecoveryRepair, RefStore};
use crate::verify::{RepositoryVerification, verify_repository};
use crate::wal::{Wal, WalRepair};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoctorSeverity {
Info,
Warning,
Error,
}
impl DoctorSeverity {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DoctorIssue {
pub code: &'static str,
pub severity: DoctorSeverity,
pub message: String,
pub recommendation: String,
}
impl DoctorIssue {
#[must_use]
pub fn info(
code: &'static str,
message: impl Into<String>,
recommendation: impl Into<String>,
) -> Self {
Self {
code,
severity: DoctorSeverity::Info,
message: message.into(),
recommendation: recommendation.into(),
}
}
#[must_use]
pub fn warning(
code: &'static str,
message: impl Into<String>,
recommendation: impl Into<String>,
) -> Self {
Self {
code,
severity: DoctorSeverity::Warning,
message: message.into(),
recommendation: recommendation.into(),
}
}
#[must_use]
pub fn error(
code: &'static str,
message: impl Into<String>,
recommendation: impl Into<String>,
) -> Self {
Self {
code,
severity: DoctorSeverity::Error,
message: message.into(),
recommendation: recommendation.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DoctorReport {
pub verification: Option<RepositoryVerification>,
pub issues: Vec<DoctorIssue>,
}
impl DoctorReport {
#[must_use]
pub fn is_healthy(&self) -> bool {
!self
.issues
.iter()
.any(|issue| issue.severity == DoctorSeverity::Error)
}
#[must_use]
pub fn count_by_severity(&self, severity: DoctorSeverity) -> usize {
self.issues
.iter()
.filter(|issue| issue.severity == severity)
.count()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DoctorRepairOptions {
pub truncate_wal_tail: bool,
pub reconstruct_main_ref: bool,
}
impl DoctorRepairOptions {
#[must_use]
pub const fn none() -> Self {
Self {
truncate_wal_tail: false,
reconstruct_main_ref: false,
}
}
#[must_use]
pub const fn truncate_wal_tail() -> Self {
Self {
truncate_wal_tail: true,
reconstruct_main_ref: false,
}
}
#[must_use]
pub const fn reconstruct_main_ref() -> Self {
Self {
truncate_wal_tail: false,
reconstruct_main_ref: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DoctorRepairReport {
pub before: DoctorReport,
pub wal_repair: WalRepair,
pub ref_repair: Option<RefRecoveryRepair>,
pub after: DoctorReport,
}
#[must_use]
pub fn doctor_repository(layout: &RepositoryLayout) -> DoctorReport {
let mut issues = Vec::new();
match verify_repository(layout) {
Ok(verification) => {
issues.push(DoctorIssue::info(
"PRIKK-DOCTOR-VERIFY-OK",
"repository verification completed without integrity errors",
"no repair action is required",
));
if verification.trailing_partial_wal_bytes != 0 {
issues.push(DoctorIssue::warning(
"PRIKK-DOCTOR-WAL-TRAILING-PARTIAL",
format!(
concat!(
"active WAL has {} trailing byte(s) that look like an incomplete ",
"final record"
),
verification.trailing_partial_wal_bytes
),
"run `prikk doctor --repair-wal-tail` to truncate only the incomplete \
final WAL bytes",
));
}
add_missing_main_ref_issue(layout, &mut issues);
DoctorReport {
verification: Some(verification),
issues,
}
}
Err(error) => {
issues.push(issue_for_verification_error(error));
DoctorReport {
verification: None,
issues,
}
}
}
}
pub fn repair_repository(
layout: &RepositoryLayout,
options: DoctorRepairOptions,
) -> Result<DoctorRepairReport> {
let before = doctor_repository(layout);
if !before.is_healthy() {
return Err(PrikkError::Integrity(
"doctor repair refused because repository verification has errors".to_string(),
));
}
let wal_repair = if options.truncate_wal_tail {
let wal = Wal::new(layout.default_queue_wal_path());
wal.truncate_trailing_partial()?
} else {
WalRepair {
preserved_records: 0,
truncated_bytes: 0,
}
};
let ref_repair = if options.reconstruct_main_ref {
let ref_store = RefStore::new(layout.clone());
Some(ref_store.reconstruct_missing_ref_from_log("heads/main")?)
} else {
None
};
let after = doctor_repository(layout);
Ok(DoctorRepairReport {
before,
wal_repair,
ref_repair,
after,
})
}
fn add_missing_main_ref_issue(layout: &RepositoryLayout, issues: &mut Vec<DoctorIssue>) {
let ref_store = RefStore::new(layout.clone());
match ref_store.recoverable_missing_ref("heads/main") {
Ok(Some(candidate)) => issues.push(DoctorIssue::warning(
"PRIKK-DOCTOR-REF-POINTER-MISSING",
format!(
"heads/main pointer is missing but ref log can recover RefState {} at update {}",
candidate.ref_state_id, candidate.update_seq
),
"run `prikk doctor --repair-main-ref` to reconstruct only the missing \
heads/main pointer from the verified ref log",
)),
Ok(None) => {}
Err(error) => issues.push(DoctorIssue::error(
"PRIKK-DOCTOR-REF-RECOVERY-ERROR",
format!("heads/main ref recovery analysis failed: {error}"),
"preserve the repository and inspect refs/logs before attempting ref repair",
)),
}
}
fn issue_for_verification_error(error: PrikkError) -> DoctorIssue {
DoctorIssue::error(
"PRIKK-DOCTOR-VERIFY-ERROR",
format!("repository verification failed: {error}"),
"do not run seal or publish operations; preserve the repository and inspect the \
failing path before attempting repair",
)
}