#![allow(dead_code)]
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct RecoveryJourney {
pub id: &'static str,
pub user_question: &'static str,
pub expected_robot_state: &'static str,
pub human_summary: &'static str,
pub safe_next_commands: &'static [&'static str],
pub unsafe_commands: &'static [&'static str],
pub required_proof: &'static str,
pub fixture_provenance: &'static str,
pub log_artifact: &'static str,
}
static JOURNEYS: &[RecoveryJourney] = &[
RecoveryJourney {
id: "first_run_no_index",
user_question: "I just installed cass — why does search return nothing?",
expected_robot_state: "db=missing, lexical=missing, recommended_action=index_full",
human_summary: "No index yet. Build the initial index; nothing to lose on a fresh install.",
safe_next_commands: &["cass index --full"],
unsafe_commands: &[],
required_proof: "cargo test --lib search::readiness (fresh_install_recommends_index_full)",
fixture_provenance: "readiness::fleet_fixtures csd_missing_lexical_metadata",
log_artifact: "proof_log scenario=first_run_no_index phase=verify",
},
RecoveryJourney {
id: "stale_searchable_semantic_unavailable",
user_question: "Search works but feels old and semantic is off — is it broken?",
expected_robot_state: "lexical=stale_but_searchable, semantic=absent, action=refresh_lexical_soon",
human_summary: "Search is correct for indexed content; refresh to pick up recent sessions. Semantic is opt-in (install a model for hybrid).",
safe_next_commands: &["cass index", "cass models install"],
unsafe_commands: &[],
required_proof: "cargo test --lib search::readiness (stale + semantic reason) + search::semantic_readiness",
fixture_provenance: "readiness::fleet_fixtures css_stale_existing_index",
log_artifact: "proof_log scenario=stale_searchable_semantic_unavailable phase=verify",
},
RecoveryJourney {
id: "high_archive_risk_backup_first",
user_question: "Search looks off on a host where the archive is the only copy — should I rebuild?",
expected_robot_state: "archive_risk=high, recommended_action=backup_then_repair",
human_summary: "High archive risk: back up the canonical archive (or produce a fingerprinted plan) BEFORE any rebuild or repair.",
safe_next_commands: &["cass doctor --json", "cass health --json"],
unsafe_commands: &["cass index --full", "cass index"],
required_proof: "cargo test --lib search::readiness (ts1_high_archive_risk_is_backup_first + archive_safety_envelope)",
fixture_provenance: "readiness::fleet_fixtures ts1_high_archive_risk",
log_artifact: "proof_log scenario=high_archive_risk_backup_first phase=verify",
},
RecoveryJourney {
id: "quarantine_excluded_incomplete_results",
user_question: "Are my search results complete, or is something being excluded?",
expected_robot_state: "lexical=ready, quarantine.quarantined_count>0 (advisory)",
human_summary: "Results are correct but a few conversations are quarantined and excluded; inspect them (search itself is unaffected).",
safe_next_commands: &["cass diag --json --quarantine"],
unsafe_commands: &[],
required_proof: "cargo test --lib indexer::quarantine + search::readiness (healthy_node_with_only_quarantine)",
fixture_provenance: "indexer/fixtures/quarantine/*.json + readiness local_stale_quarantine",
log_artifact: "proof_log scenario=quarantine_excluded_incomplete_results phase=verify",
},
RecoveryJourney {
id: "unreachable_fleet_host",
user_question: "A fleet host won't respond — what do I do locally?",
expected_robot_state: "db=unreachable, recommended_action=host_unreachable",
human_summary: "Host unreachable; nothing local is trustworthy. Retry from a reachable node before acting.",
safe_next_commands: &["cass status --json"],
unsafe_commands: &[],
required_proof: "cargo test --lib search::readiness (mac_mini_old_unreachable_yields_host_unreachable)",
fixture_provenance: "readiness::fleet_fixtures mac_mini_old_unreachable",
log_artifact: "proof_log scenario=unreachable_fleet_host phase=verify",
},
RecoveryJourney {
id: "watch_crash_recovery",
user_question: "cass index --watch keeps exiting — is it looping on a rebuild?",
expected_robot_state: "watch_exit kind=storage_close_failure/timeout; recovery=bounded (not full rebuild)",
human_summary: "Watch emitted a parseable exit envelope; recovery is bounded (resume/repair), not a full-rebuild loop. Check health, then re-run watch.",
safe_next_commands: &["cass health --json", "cass index --watch"],
unsafe_commands: &[],
required_proof: "cargo test --lib search::watch_exit_envelope + search::watch_recovery + search::liveness_fixtures",
fixture_provenance: "search::liveness_fixtures watch_recovery_fixtures",
log_artifact: "proof_log scenario=watch_crash_recovery phase=verify",
},
];
pub(crate) fn recovery_journeys() -> &'static [RecoveryJourney] {
JOURNEYS
}
pub(crate) fn journey(id: &str) -> Option<&'static RecoveryJourney> {
JOURNEYS.iter().find(|j| j.id == id)
}
#[cfg(test)]
mod tests {
use super::*;
const REQUIRED: &[&str] = &[
"first_run_no_index",
"stale_searchable_semantic_unavailable",
"high_archive_risk_backup_first",
"quarantine_excluded_incomplete_results",
];
#[test]
fn required_journeys_are_all_present() {
for id in REQUIRED {
assert!(journey(id).is_some(), "missing required journey {id}");
}
}
#[test]
fn every_journey_names_all_acceptance_fields() {
for j in recovery_journeys() {
assert!(!j.user_question.is_empty(), "{} user_question", j.id);
assert!(!j.expected_robot_state.is_empty(), "{} robot_state", j.id);
assert!(!j.human_summary.is_empty(), "{} human_summary", j.id);
assert!(!j.safe_next_commands.is_empty(), "{} safe commands", j.id);
assert!(!j.required_proof.is_empty(), "{} proof", j.id);
assert!(!j.fixture_provenance.is_empty(), "{} provenance", j.id);
assert!(!j.log_artifact.is_empty(), "{} log artifact", j.id);
}
}
#[test]
fn safe_commands_are_concrete_cass_and_never_destructive() {
for j in recovery_journeys() {
for cmd in j.safe_next_commands {
assert!(
cmd.starts_with("cass "),
"{}: safe command must be a concrete cass invocation: {cmd}",
j.id
);
for bad in ["rm ", "rm -", "--force-clean", "DROP ", "delete "] {
assert!(
!cmd.contains(bad),
"{} destructive safe command: {cmd}",
j.id
);
}
}
}
}
#[test]
fn only_high_archive_risk_journey_gates_unsafe_commands() {
let high = journey("high_archive_risk_backup_first").unwrap();
assert!(!high.unsafe_commands.is_empty());
assert!(
high.unsafe_commands
.iter()
.any(|c| c.contains("index --full"))
);
assert!(high.safe_next_commands.iter().all(|c| !c.contains("index")));
for j in recovery_journeys() {
if j.id != "high_archive_risk_backup_first" {
assert!(
j.unsafe_commands.is_empty(),
"{} should not gate unsafe commands",
j.id
);
}
}
}
#[test]
fn first_run_never_marks_index_unsafe() {
let fr = journey("first_run_no_index").unwrap();
assert!(fr.safe_next_commands.contains(&"cass index --full"));
assert!(fr.unsafe_commands.is_empty());
}
#[test]
fn journey_serializes_with_expected_fields() {
let j = journey("high_archive_risk_backup_first").unwrap();
let json = serde_json::to_string(j).unwrap();
assert!(json.contains("\"id\":\"high_archive_risk_backup_first\""));
assert!(json.contains("\"unsafe_commands\":[\"cass index --full\""));
assert!(json.contains("\"required_proof\""));
assert!(json.contains("\"log_artifact\""));
}
#[test]
fn journeys_are_deterministic_in_order() {
let a: Vec<&str> = recovery_journeys().iter().map(|j| j.id).collect();
assert_eq!(a.first(), Some(&"first_run_no_index"));
assert_eq!(
a,
recovery_journeys().iter().map(|j| j.id).collect::<Vec<_>>()
);
}
}