#![allow(dead_code)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ZeroResultDiagnosis {
GenuineNoMatch,
WorkspaceFilterLikelyWrong,
WorkspaceHasNoMatch,
SourceIdFilter,
WorkspaceNotIndexed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum MatchKind {
Exact,
CaseInsensitive,
PathNormalized,
PlatformPathVariant,
BasenameMoved,
}
impl MatchKind {
fn confidence(self) -> Confidence {
match self {
Self::Exact | Self::CaseInsensitive | Self::PathNormalized => Confidence::High,
Self::PlatformPathVariant => Confidence::Medium,
Self::BasenameMoved => Confidence::Low,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum Confidence {
Low,
Medium,
High,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct WorkspaceCandidate {
pub workspace: String,
pub match_kind: MatchKind,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ZeroResultReport {
pub diagnosis: ZeroResultDiagnosis,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub candidate_workspaces: Vec<WorkspaceCandidate>,
pub confidence: Confidence,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suggested_rerun: Option<String>,
}
fn normalize_path(s: &str) -> String {
let trimmed = s.trim();
let no_lead = trimmed.strip_prefix("./").unwrap_or(trimmed);
let collapsed = no_lead.replace("//", "/");
collapsed
.strip_suffix('/')
.unwrap_or(&collapsed)
.to_string()
}
fn normalize_case(s: &str) -> String {
normalize_path(s).to_ascii_lowercase()
}
fn platform_variant(s: &str) -> Option<String> {
let n = normalize_path(s);
if let Some(rest) = n.strip_prefix("/Users/") {
Some(format!("/home/{rest}"))
} else {
n.strip_prefix("/home/")
.map(|rest| format!("/Users/{rest}"))
}
}
fn basename(s: &str) -> &str {
let n = s.trim().trim_end_matches('/');
n.rsplit('/').next().unwrap_or(n)
}
fn match_kind(requested: &str, known: &str) -> Option<MatchKind> {
if requested == known {
return Some(MatchKind::Exact);
}
if normalize_path(requested) == normalize_path(known) {
return Some(MatchKind::PathNormalized);
}
if normalize_case(requested) == normalize_case(known) {
return Some(MatchKind::CaseInsensitive);
}
if let Some(variant) = platform_variant(requested)
&& normalize_case(&variant) == normalize_case(known)
{
return Some(MatchKind::PlatformPathVariant);
}
let rb = basename(requested);
if !rb.is_empty() && normalize_case(rb) == normalize_case(basename(known)) {
return Some(MatchKind::BasenameMoved);
}
None
}
fn looks_like_source_id(requested: &str) -> bool {
let t = requested.trim();
!t.is_empty() && t.chars().all(|c| c.is_ascii_digit())
}
pub(crate) fn diagnose_zero_result(
requested_workspace: &str,
known_workspaces: &[String],
global_had_hits: bool,
) -> ZeroResultReport {
if looks_like_source_id(requested_workspace) {
return ZeroResultReport {
diagnosis: ZeroResultDiagnosis::SourceIdFilter,
candidate_workspaces: Vec::new(),
confidence: Confidence::High,
suggested_rerun: Some(
"list sources with `cass sources list --json` and filter by an existing source_id"
.to_string(),
),
};
}
if !global_had_hits {
return ZeroResultReport {
diagnosis: ZeroResultDiagnosis::GenuineNoMatch,
candidate_workspaces: Vec::new(),
confidence: Confidence::High,
suggested_rerun: None,
};
}
let mut candidates: Vec<WorkspaceCandidate> = known_workspaces
.iter()
.filter_map(|known| {
match_kind(requested_workspace, known).map(|kind| WorkspaceCandidate {
workspace: known.clone(),
match_kind: kind,
confidence: kind.confidence(),
})
})
.collect();
candidates.sort_by(|a, b| {
a.match_kind
.cmp(&b.match_kind)
.then_with(|| a.workspace.cmp(&b.workspace))
});
if matches!(
candidates.first().map(|c| c.match_kind),
Some(MatchKind::Exact)
) {
return ZeroResultReport {
diagnosis: ZeroResultDiagnosis::WorkspaceHasNoMatch,
candidate_workspaces: Vec::new(),
confidence: Confidence::High,
suggested_rerun: None,
};
}
if candidates.is_empty() {
return ZeroResultReport {
diagnosis: ZeroResultDiagnosis::WorkspaceNotIndexed,
candidate_workspaces: Vec::new(),
confidence: Confidence::Low,
suggested_rerun: None,
};
}
let top = &candidates[0];
let overall = top.confidence;
let suggested_rerun = Some(format!(
"re-run with the canonical workspace: --workspace \"{}\"",
top.workspace
));
ZeroResultReport {
diagnosis: ZeroResultDiagnosis::WorkspaceFilterLikelyWrong,
candidate_workspaces: candidates,
confidence: overall,
suggested_rerun,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ws(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn enums_serialize_as_snake_case() {
assert_eq!(
serde_json::to_string(&ZeroResultDiagnosis::WorkspaceFilterLikelyWrong).unwrap(),
"\"workspace_filter_likely_wrong\""
);
assert_eq!(
serde_json::to_string(&MatchKind::PlatformPathVariant).unwrap(),
"\"platform_path_variant\""
);
assert_eq!(
serde_json::to_string(&Confidence::High).unwrap(),
"\"high\""
);
}
#[test]
fn genuine_no_match_when_global_query_also_empty() {
let r = diagnose_zero_result("/home/u/proj", &ws(&["/home/u/other"]), false);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::GenuineNoMatch);
assert!(r.candidate_workspaces.is_empty());
assert!(r.suggested_rerun.is_none());
}
#[test]
fn exact_workspace_with_no_hits_is_per_workspace_empty_not_wrong_filter() {
let r = diagnose_zero_result(
"/home/u/proj",
&ws(&["/home/u/proj", "/home/u/other"]),
true,
);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::WorkspaceHasNoMatch);
assert!(r.candidate_workspaces.is_empty());
}
#[test]
fn case_insensitive_miss_suggests_canonical_with_high_confidence() {
let r = diagnose_zero_result("/Home/U/Proj", &ws(&["/home/u/proj"]), true);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::WorkspaceFilterLikelyWrong);
assert_eq!(r.candidate_workspaces.len(), 1);
assert_eq!(
r.candidate_workspaces[0].match_kind,
MatchKind::CaseInsensitive
);
assert_eq!(r.confidence, Confidence::High);
assert!(r.suggested_rerun.unwrap().contains("/home/u/proj"));
}
#[test]
fn trailing_slash_is_path_normalized_match() {
let r = diagnose_zero_result("/home/u/proj/", &ws(&["/home/u/proj"]), true);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::WorkspaceFilterLikelyWrong);
assert_eq!(
r.candidate_workspaces[0].match_kind,
MatchKind::PathNormalized
);
assert_eq!(r.confidence, Confidence::High);
}
#[test]
fn macos_linux_path_variant_is_medium_confidence() {
let r = diagnose_zero_result("/Users/jeff/cass", &ws(&["/home/jeff/cass"]), true);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::WorkspaceFilterLikelyWrong);
assert_eq!(
r.candidate_workspaces[0].match_kind,
MatchKind::PlatformPathVariant
);
assert_eq!(r.confidence, Confidence::Medium);
}
#[test]
fn moved_checkout_matches_on_basename_with_low_confidence() {
let r = diagnose_zero_result(
"/old/location/myproj",
&ws(&["/new/home/myproj", "/unrelated/thing"]),
true,
);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::WorkspaceFilterLikelyWrong);
assert_eq!(r.candidate_workspaces[0].workspace, "/new/home/myproj");
assert_eq!(
r.candidate_workspaces[0].match_kind,
MatchKind::BasenameMoved
);
assert_eq!(r.confidence, Confidence::Low);
}
#[test]
fn source_id_filter_is_recognized_and_not_workspace_suggested() {
let r = diagnose_zero_result("42", &ws(&["/home/u/proj"]), true);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::SourceIdFilter);
assert!(r.candidate_workspaces.is_empty());
assert!(r.suggested_rerun.unwrap().contains("sources list"));
}
#[test]
fn no_close_workspace_is_not_indexed() {
let r = diagnose_zero_result(
"/home/u/ghost",
&ws(&["/home/u/real", "/var/data/thing"]),
true,
);
assert_eq!(r.diagnosis, ZeroResultDiagnosis::WorkspaceNotIndexed);
assert!(r.candidate_workspaces.is_empty());
assert_eq!(r.confidence, Confidence::Low);
}
#[test]
fn candidates_are_ranked_strongest_match_first() {
let r = diagnose_zero_result(
"/HOME/U/PROJ",
&ws(&["/elsewhere/proj", "/home/u/proj"]),
true,
);
assert_eq!(
r.candidate_workspaces[0].match_kind,
MatchKind::CaseInsensitive
);
assert_eq!(r.candidate_workspaces[0].workspace, "/home/u/proj");
assert_eq!(
r.candidate_workspaces[1].match_kind,
MatchKind::BasenameMoved
);
}
#[test]
fn report_round_trips_through_json() {
let r = diagnose_zero_result("/Users/jeff/cass", &ws(&["/home/jeff/cass"]), true);
let json = serde_json::to_string(&r).unwrap();
assert!(json.contains("\"diagnosis\":\"workspace_filter_likely_wrong\""));
assert!(json.contains("\"match_kind\":\"platform_path_variant\""));
let parsed: ZeroResultReport = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, r);
}
}