use serde::{Deserialize, Serialize};
use crate::search::readiness::{
ArchiveRiskLevel, BinaryCompatibility, CanonicalDbAvailability, DerivedAssetTruthTable,
LexicalReadinessState, SafeNextAction, SemanticReadinessState, SourceCoverageState,
};
use crate::search::readiness_projection::{ReadinessClass, ReadinessSummary, SurfaceKind, project};
pub(crate) const UNSAFE_COMMAND_FRAGMENTS: &[&str] = &[
"rm -rf",
"rm -r ",
"rm -f",
" rm ",
"rmdir",
"--delete",
"reset --hard",
"git clean",
"checkout --",
"drop table",
"drop database",
"truncate",
"mkfs",
"dd if=",
"shred",
"--purge",
"> /dev/sd",
];
const SAFE_COMMAND_PREFIXES: &[&str] = &[
"cass diag",
"cass health",
"cass status",
"cass doctor",
"cass index",
"cass sources",
"cass models",
"cass self-update",
"cass triage",
];
pub(crate) fn command_is_safe(command: Option<&str>) -> bool {
let Some(cmd) = command else {
return true;
};
let lower = cmd.to_ascii_lowercase();
if UNSAFE_COMMAND_FRAGMENTS
.iter()
.any(|frag| lower.contains(frag))
{
return false;
}
SAFE_COMMAND_PREFIXES
.iter()
.any(|prefix| lower.starts_with(prefix))
}
pub(crate) fn text_is_clean(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
!UNSAFE_COMMAND_FRAGMENTS
.iter()
.any(|frag| lower.contains(frag))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct HumanReadinessSummary {
pub surface: SurfaceKind,
pub class: ReadinessClass,
pub headline: String,
pub search_usable_now: bool,
pub why_it_matters: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub whats_missing_or_stale: Option<String>,
pub safest_next_action: String,
pub safe_next_action: SafeNextAction,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub safe_command: Option<String>,
pub state_codes: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deferred_fields: Vec<String>,
pub partial: bool,
}
const MAX_RENDER_LINES: usize = 7;
const MAX_LINE_LEN: usize = 200;
impl HumanReadinessSummary {
pub(crate) fn render_lines(&self) -> Vec<String> {
let mut lines = Vec::with_capacity(MAX_RENDER_LINES);
lines.push(self.headline.clone());
lines.push(format!(
" Search usable now: {}",
if self.search_usable_now { "yes" } else { "no" }
));
lines.push(format!(" Why: {}", self.why_it_matters));
if let Some(missing) = &self.whats_missing_or_stale {
lines.push(format!(" Missing/stale: {missing}"));
}
let action_line = match &self.safe_command {
Some(cmd) => format!(" Safest next step: {} → `{cmd}`", self.safest_next_action),
None => format!(" Safest next step: {}", self.safest_next_action),
};
lines.push(action_line);
if self.partial && !self.deferred_fields.is_empty() {
lines.push(format!(
" (compact: deferred {})",
self.deferred_fields.join(", ")
));
}
lines.push(format!(" State codes: {}", self.state_codes));
lines.truncate(MAX_RENDER_LINES);
for line in &mut lines {
if line.chars().count() > MAX_LINE_LEN {
let truncated: String = line.chars().take(MAX_LINE_LEN - 1).collect();
*line = format!("{truncated}…");
}
}
lines
}
pub(crate) fn render_compact_line(&self) -> String {
let usable = if self.search_usable_now {
"searchable"
} else {
"not searchable"
};
let mut line = format!(
"{} · {usable} · next: {}",
self.headline, self.safest_next_action
);
if line.chars().count() > MAX_LINE_LEN {
let truncated: String = line.chars().take(MAX_LINE_LEN - 1).collect();
line = format!("{truncated}…");
}
line
}
pub(crate) fn is_safe(&self, archive_risk: ArchiveRiskLevel) -> bool {
if !command_is_safe(self.safe_command.as_deref()) {
return false;
}
let prose_clean = [
self.headline.as_str(),
self.why_it_matters.as_str(),
self.safest_next_action.as_str(),
]
.iter()
.chain(self.whats_missing_or_stale.as_deref().iter())
.all(|t| text_is_clean(t));
if !prose_clean {
return false;
}
if archive_risk == ArchiveRiskLevel::High && self.safe_next_action.is_mutating() {
return false;
}
true
}
}
fn code<T: Serialize>(value: &T) -> String {
serde_json::to_string(value)
.ok()
.map(|s| s.trim_matches('"').to_string())
.unwrap_or_else(|| "unknown".to_string())
}
pub(crate) fn project_human_summary(
table: &DerivedAssetTruthTable,
surface: SurfaceKind,
) -> HumanReadinessSummary {
let robot: ReadinessSummary = project(table, surface);
let safe_cmd = table.safe_next_command();
let headline = headline_for(robot.class);
let why_it_matters = why_for(robot.class).to_string();
let whats_missing_or_stale = missing_or_stale_note(table, &robot);
let state_codes = format!(
"class={} db={} lexical={} semantic={} source={} archive_risk={} binary={} next={}",
code(&robot.class),
code(&table.db),
code(&table.readiness.lexical),
code(&table.readiness.semantic),
code(&table.source_coverage),
code(&table.archive_risk),
code(&table.binary),
code(&safe_cmd.action),
);
HumanReadinessSummary {
surface,
class: robot.class,
headline,
search_usable_now: robot.is_searchable,
why_it_matters,
whats_missing_or_stale,
safest_next_action: safe_cmd.reason.clone(),
safe_next_action: safe_cmd.action,
safe_command: safe_cmd.command.clone(),
state_codes,
deferred_fields: robot.deferred_fields.clone(),
partial: !robot.deferred_fields.is_empty(),
}
}
fn headline_for(class: ReadinessClass) -> String {
match class {
ReadinessClass::Ready => "✓ Search ready",
ReadinessClass::StaleSearchable => "≈ Search ready (index stale)",
ReadinessClass::Repairing => "↻ Search degraded (rebuild in progress)",
ReadinessClass::Missing => "✗ Search unavailable (no index)",
ReadinessClass::CorruptQuarantined => "✗ Search unavailable (index corrupt/quarantined)",
ReadinessClass::DbUnusable => "✗ Search unavailable (database unusable)",
ReadinessClass::Unreachable => "? Host unreachable",
}
.to_string()
}
fn why_for(class: ReadinessClass) -> &'static str {
match class {
ReadinessClass::Ready => {
"All derived search assets are converged; results are complete and current."
}
ReadinessClass::StaleSearchable => {
"Search is fully correct for everything already indexed; only the most recent \
sessions may not appear yet."
}
ReadinessClass::Repairing => {
"A rebuild is running; queries still work but may return partial results until it \
settles — wait or attach, do not start a second rebuild."
}
ReadinessClass::Missing => {
"No usable lexical index exists yet, so search returns nothing until it is rebuilt \
from the canonical archive."
}
ReadinessClass::CorruptQuarantined => {
"The lexical index failed validation and was quarantined; it needs inspection before \
any auto-recovery is safe."
}
ReadinessClass::DbUnusable => {
"The canonical database could not be opened or validated; inspect it read-only first \
— never blind-rebuild over it."
}
ReadinessClass::Unreachable => {
"The host backing this node did not answer the probe, so no local state is \
trustworthy; retry from a reachable node."
}
}
}
fn missing_or_stale_note(
table: &DerivedAssetTruthTable,
robot: &ReadinessSummary,
) -> Option<String> {
let mut notes: Vec<String> = Vec::new();
let compact = !robot.deferred_fields.is_empty();
match table.source_coverage {
SourceCoverageState::Unconfigured => {
notes.push(
"no sources are configured (source=unconfigured) — nothing indexed yet".into(),
);
}
SourceCoverageState::Unavailable => notes.push(
"configured sources are unreachable (source=unavailable); the archive is preserved — \
reconnect before syncing, do not remove the source"
.into(),
),
SourceCoverageState::Partial => notes.push(
"some configured sources are unreachable (source=partial); the index reflects the \
reachable subset"
.into(),
),
SourceCoverageState::Complete | SourceCoverageState::Unknown => {}
}
if table.readiness.lexical == LexicalReadinessState::StaleButSearchable {
notes.push(
"lexical index lags recent ingests (lexical=stale_but_searchable) — search works, a \
refresh picks up the newest sessions"
.into(),
);
}
if table.db == CanonicalDbAvailability::Available && table.readiness.lexical.is_searchable() {
match table.readiness.semantic {
SemanticReadinessState::Absent => notes.push(
"semantic refinement unavailable (semantic=absent); results are lexical-only until \
a model is installed"
.into(),
),
SemanticReadinessState::Backfilling => notes.push(
"semantic refinement still backfilling (semantic=backfilling); hybrid quality \
improves on its own"
.into(),
),
SemanticReadinessState::PolicyDisabled => notes.push(
"semantic refinement disabled by policy (semantic=policy_disabled) — intentional, \
not a fault"
.into(),
),
SemanticReadinessState::FastTierReady | SemanticReadinessState::HybridReady => {}
}
}
match table.binary {
BinaryCompatibility::Outdated => notes.push(
"this binary is older than the fleet baseline (binary=outdated) — upgrade before \
trusting or rebuilding assets"
.into(),
),
BinaryCompatibility::Ahead => notes.push(
"this binary is newer than the on-disk assets (binary=ahead) — rebuild assets for the \
current schema"
.into(),
),
BinaryCompatibility::Current | BinaryCompatibility::Unknown => {}
}
if robot.quarantine_incomplete {
if compact {
notes
.push("quarantined artifacts present (advisory; detail deferred for speed)".into());
} else {
notes.push(format!(
"{} artifact(s) quarantined (advisory; results are unaffected)",
table.quarantine.quarantined_count
));
}
}
if notes.is_empty() {
return None;
}
notes.truncate(3);
Some(notes.join("; "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::search::readiness::{
ArchiveRiskLevel, LexicalMetadata, MaintenanceActivity, QuarantineSummary,
ReadinessSnapshot, fleet_fixtures,
};
use std::collections::BTreeMap;
fn fixture(name: &str) -> DerivedAssetTruthTable {
fleet_fixtures()
.into_iter()
.find(|(n, _)| *n == name)
.expect("missing fleet fixture (name not present in fleet_fixtures)")
.1
}
fn converged() -> DerivedAssetTruthTable {
DerivedAssetTruthTable {
db: CanonicalDbAvailability::Available,
source_coverage: SourceCoverageState::Complete,
scan_watermark_ms: Some(1),
last_projection_ms: Some(1),
lexical_metadata: LexicalMetadata {
present: true,
schema_hash: Some("schema-current".into()),
storage_fingerprint: Some("fp-current".into()),
built_at_ms: Some(1),
},
readiness: ReadinessSnapshot::new(
LexicalReadinessState::Ready,
SemanticReadinessState::HybridReady,
),
quarantine: QuarantineSummary::default(),
maintenance: MaintenanceActivity::Idle,
archive_risk: ArchiveRiskLevel::None,
binary: BinaryCompatibility::Current,
}
}
fn with_lexical_semantic(
lexical: LexicalReadinessState,
semantic: SemanticReadinessState,
) -> DerivedAssetTruthTable {
let mut t = converged();
t.readiness = ReadinessSnapshot::new(lexical, semantic);
t
}
#[test]
fn summary_serializes_and_round_trips() {
let s = project_human_summary(&converged(), SurfaceKind::Status);
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("\"class\":\"ready\""));
assert!(json.contains("\"search_usable_now\":true"));
let parsed: HumanReadinessSummary = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, s);
}
#[test]
fn headline_distinguishes_every_class() {
let classes = [
ReadinessClass::Ready,
ReadinessClass::StaleSearchable,
ReadinessClass::Repairing,
ReadinessClass::Missing,
ReadinessClass::CorruptQuarantined,
ReadinessClass::DbUnusable,
ReadinessClass::Unreachable,
];
let headlines: BTreeMap<String, ()> =
classes.iter().map(|c| (headline_for(*c), ())).collect();
assert_eq!(headlines.len(), classes.len(), "headlines must be distinct");
}
#[test]
fn stale_searchable_is_distinct_from_missing() {
let stale =
project_human_summary(&fixture("css_stale_existing_index"), SurfaceKind::Status);
let missing = project_human_summary(
&fixture("csd_missing_lexical_metadata"),
SurfaceKind::Status,
);
assert_eq!(stale.class, ReadinessClass::StaleSearchable);
assert!(stale.search_usable_now, "stale index is still searchable");
assert_eq!(missing.class, ReadinessClass::Missing);
assert!(
!missing.search_usable_now,
"missing index is not searchable"
);
assert_ne!(stale.headline, missing.headline);
assert_ne!(stale.safe_next_action, missing.safe_next_action);
}
#[test]
fn semantic_absent_does_not_read_as_lexical_broken() {
let s = project_human_summary(
&with_lexical_semantic(LexicalReadinessState::Ready, SemanticReadinessState::Absent),
SurfaceKind::Status,
);
assert!(s.search_usable_now);
assert_eq!(s.class, ReadinessClass::Ready);
let missing = s.whats_missing_or_stale.as_deref().unwrap_or_default();
assert!(missing.contains("semantic=absent"), "got: {missing}");
assert_eq!(s.safe_next_action, SafeNextAction::InstallSemanticModel);
}
#[test]
fn unreachable_source_is_distinct_from_removed_source() {
let unavailable = {
let mut t = converged();
t.source_coverage = SourceCoverageState::Unavailable;
project_human_summary(&t, SurfaceKind::Status)
};
let unconfigured = {
let mut t = converged();
t.source_coverage = SourceCoverageState::Unconfigured;
project_human_summary(&t, SurfaceKind::Status)
};
let unavailable_note = unavailable.whats_missing_or_stale.unwrap_or_default();
let unconfigured_note = unconfigured.whats_missing_or_stale.unwrap_or_default();
assert!(unavailable_note.contains("source=unavailable"));
assert!(
unavailable_note.contains("do not remove the source"),
"unreachable must warn against removal: {unavailable_note}"
);
assert!(unconfigured_note.contains("source=unconfigured"));
assert_ne!(unavailable_note, unconfigured_note);
}
#[test]
fn quarantine_incomplete_is_distinct_from_no_results() {
let s = project_human_summary(&fixture("local_stale_quarantine"), SurfaceKind::Status);
assert!(
s.search_usable_now,
"quarantine never makes search unusable"
);
let note = s.whats_missing_or_stale.unwrap_or_default();
assert!(note.contains("quarantined"), "got: {note}");
assert!(note.contains("advisory"), "quarantine is advisory: {note}");
}
#[test]
fn human_summary_never_contradicts_robot_for_any_fleet_state() {
for (name, table) in fleet_fixtures() {
for surface in [
SurfaceKind::Health,
SurfaceKind::Status,
SurfaceKind::Triage,
SurfaceKind::SearchMeta,
] {
let robot = project(&table, surface);
let human = project_human_summary(&table, surface);
assert_eq!(human.class, robot.class, "{name}/{surface:?} class");
assert_eq!(
human.search_usable_now, robot.is_searchable,
"{name}/{surface:?} searchable"
);
assert_eq!(
human.safe_next_action, robot.safe_next_action,
"{name}/{surface:?} next action"
);
assert!(
human
.state_codes
.contains(&format!("class={}", code(&robot.class))),
"{name}/{surface:?} class code missing from human copy"
);
assert!(
human
.state_codes
.contains(&format!("next={}", code(&robot.safe_next_action))),
"{name}/{surface:?} next code missing from human copy"
);
let proof = serde_json::json!({
"event": "human_robot_parity",
"fixture": name,
"surface": code(&surface),
"robot": {
"class": code(&robot.class),
"is_searchable": robot.is_searchable,
"next": code(&robot.safe_next_action),
},
"human": {
"class": code(&human.class),
"search_usable_now": human.search_usable_now,
"next": code(&human.safe_next_action),
"state_codes": human.state_codes,
},
"parity": true,
});
println!("{proof}");
}
}
}
#[test]
fn no_summary_surfaces_a_destructive_command() {
let mut tables: Vec<(String, DerivedAssetTruthTable)> = fleet_fixtures()
.into_iter()
.map(|(n, t)| (n.to_string(), t))
.collect();
tables.push(("converged".into(), converged()));
tables.push(("high_risk".into(), fixture("ts1_high_archive_risk")));
for (name, table) in tables {
for surface in [SurfaceKind::Health, SurfaceKind::Status] {
let human = project_human_summary(&table, surface);
assert!(
human.is_safe(table.archive_risk),
"{name}/{surface:?} produced an unsafe summary: {:?}",
human.safe_command
);
for line in human.render_lines() {
assert!(
text_is_clean(&line),
"{name}/{surface:?} unsafe line: {line}"
);
}
}
}
}
#[test]
fn high_archive_risk_is_backup_first_never_a_rebuild() {
let human = project_human_summary(&fixture("ts1_high_archive_risk"), SurfaceKind::Status);
assert_eq!(human.safe_next_action, SafeNextAction::BackupThenRepair);
assert!(!human.safe_next_action.is_mutating());
assert!(human.is_safe(ArchiveRiskLevel::High));
if let Some(cmd) = &human.safe_command {
assert!(cmd.starts_with("cass doctor"), "got: {cmd}");
}
}
#[test]
fn command_safety_allow_list_rejects_destructive_and_bare_cass() {
assert!(command_is_safe(Some("cass index --full")));
assert!(command_is_safe(Some("cass diag --json --quarantine")));
assert!(command_is_safe(None));
assert!(!command_is_safe(Some("rm -rf ~/.local/share/cass")));
assert!(!command_is_safe(Some("cass index --full && rm -rf foo")));
assert!(!command_is_safe(Some("cass")), "bare cass launches the TUI");
assert!(!command_is_safe(Some("git reset --hard")));
assert!(!command_is_safe(Some("cass sources remove laptop --purge")));
}
#[test]
fn render_is_bounded() {
for (_, table) in fleet_fixtures() {
let human = project_human_summary(&table, SurfaceKind::Status);
let lines = human.render_lines();
assert!(lines.len() <= MAX_RENDER_LINES, "too many lines");
for line in &lines {
assert!(
line.chars().count() <= MAX_LINE_LEN,
"line too long: {line}"
);
}
assert!(human.render_compact_line().chars().count() <= MAX_LINE_LEN);
}
}
#[test]
fn cheap_health_surface_degrades_to_compact_partial() {
let table = fixture("local_stale_quarantine");
let health = project_human_summary(&table, SurfaceKind::Health);
let status = project_human_summary(&table, SurfaceKind::Status);
assert!(health.partial, "health is a compact partial");
assert!(!health.deferred_fields.is_empty(), "health marks deferrals");
assert!(!status.partial, "status probes fully");
assert!(status.deferred_fields.is_empty());
assert_eq!(health.class, status.class);
assert_eq!(health.search_usable_now, status.search_usable_now);
assert_eq!(health.safe_next_action, status.safe_next_action);
let health_note = health.whats_missing_or_stale.clone().unwrap_or_default();
let status_note = status.whats_missing_or_stale.clone().unwrap_or_default();
assert!(
health_note.contains("deferred"),
"health note: {health_note}"
);
assert!(
status_note.contains("quarantined"),
"status note: {status_note}"
);
}
fn golden_cases() -> Vec<(String, DerivedAssetTruthTable)> {
let mut cases: Vec<(String, DerivedAssetTruthTable)> = fleet_fixtures()
.into_iter()
.map(|(n, t)| (n.to_string(), t))
.collect();
cases.push(("converged_all_green".into(), converged()));
cases.push((
"ready_semantic_absent".into(),
with_lexical_semantic(LexicalReadinessState::Ready, SemanticReadinessState::Absent),
));
cases.push((
"ready_semantic_policy_disabled".into(),
with_lexical_semantic(
LexicalReadinessState::Ready,
SemanticReadinessState::PolicyDisabled,
),
));
cases
}
fn render_golden_document() -> String {
let mut out = String::new();
out.push_str("# Golden: human readiness summaries (bead 13.2)\n");
out.push_str("# Regenerate: RCH_CARGO_WRAPPER_BYPASS=1 UPDATE_GOLDENS=1 cargo test \\\n");
out.push_str("# --lib search::human_readiness_summary::tests::golden_human_summaries\n");
for (name, table) in golden_cases() {
let human = project_human_summary(&table, SurfaceKind::Status);
out.push_str("\n========================================\n");
out.push_str(&format!("case: {name}\n"));
out.push_str("----------------------------------------\n");
for line in human.render_lines() {
out.push_str(&line);
out.push('\n');
}
}
out
}
#[test]
fn golden_human_summaries() {
let golden_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/golden/human/readiness_summaries.txt"
);
let rendered = render_golden_document();
if std::env::var("UPDATE_GOLDENS").is_ok() {
if let Some(parent) = std::path::Path::new(golden_path).parent() {
std::fs::create_dir_all(parent).expect("create golden dir");
}
std::fs::write(golden_path, &rendered).expect("write golden");
return;
}
let expected = std::fs::read_to_string(golden_path).expect(
"missing golden tests/golden/human/readiness_summaries.txt — regenerate with \
UPDATE_GOLDENS=1 (see AGENTS.md)",
);
assert_eq!(
rendered, expected,
"human readiness golden drift — review then regenerate with UPDATE_GOLDENS=1"
);
}
}