use super::*;
impl AddressSet {
pub(crate) fn addresses(&self, kept: &Kept) -> bool {
(!self.lines.is_empty() && self.lines.contains(&kept.line_no))
|| (!self.uuids.is_empty()
&& kept
.rec
.uuid
.as_deref()
.is_some_and(|u| self.uuids.contains(u)))
}
}
pub(crate) fn record_groups(hits: &[Hit]) -> Vec<&[Hit]> {
let mut out = Vec::new();
let mut i = 0;
while i < hits.len() {
let mut j = i + 1;
if hits[i].line > 0 {
while j < hits.len() && hits[j].line == hits[i].line {
j += 1;
}
}
out.push(&hits[i..j]);
i = j;
}
out
}
pub(crate) fn label_census(
exchanges: &[Exchange],
filter: LabelFilter<'_>,
) -> (BTreeMap<&'static str, usize>, usize) {
let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new();
let mut records = 0usize;
for ex in exchanges {
for group in record_groups(&ex.hits) {
records += 1;
for &leaf in &group[0].labels {
if filter.selected(leaf) {
*counts.entry(leaf).or_insert(0) += 1;
}
}
}
}
(counts, records)
}
pub(crate) fn axis_census(
exchanges: &[Exchange],
axis: crate::cli::CountAxis,
filter: LabelFilter<'_>,
) -> (Vec<(String, usize)>, usize, usize) {
use crate::cli::CountAxis as A;
let multi_transcript = distinct_session_count(exchanges) > 1;
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
let mut turn_counts: BTreeMap<(String, usize), usize> = BTreeMap::new();
let mut records = 0usize;
let mut excluded = 0usize;
for ex in exchanges {
for group in record_groups(&ex.hits) {
records += 1;
match axis {
A::Label => {
for &leaf in &group[0].labels {
if filter.selected(leaf) {
*counts.entry(leaf.to_string()).or_insert(0) += 1;
}
}
}
A::Tool => match group.iter().find_map(|h| h.tool_name.clone()) {
Some(t) => *counts.entry(t).or_insert(0) += 1,
None => excluded += 1,
},
A::Turn => {
*turn_counts
.entry((ex.session_id.clone(), ex.turn_index))
.or_insert(0) += 1;
}
A::Session => *counts.entry(ex.session_id.clone()).or_insert(0) += 1,
A::Pairing => match group.iter().find_map(|h| h.pair) {
Some(Pairing::Paired) => *counts.entry("paired".to_string()).or_insert(0) += 1,
Some(Pairing::PendingNoResult) => {
*counts.entry("pending".to_string()).or_insert(0) += 1;
}
Some(Pairing::OrphanResult) => {
*counts.entry("orphan".to_string()).or_insert(0) += 1;
}
None => excluded += 1,
},
A::Model => match group.iter().find_map(|h| h.model.clone()) {
Some(m) => *counts.entry(m).or_insert(0) += 1,
None => excluded += 1,
},
}
}
}
let rows: Vec<(String, usize)> = if matches!(axis, A::Turn) {
turn_counts
.into_iter()
.map(|((sid, t), n)| {
let key = if multi_transcript {
format!("{sid}\u{b7}t{t}")
} else {
format!("t{t}")
};
(key, n)
})
.collect()
} else {
let mut v: Vec<(String, usize)> = counts.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
v
};
(rows, records, excluded)
}
#[derive(Debug)]
pub(crate) struct EmptyDiagnosis {
pub(crate) sessions_in_scope: usize,
pub(crate) active_filters: String,
pub(crate) skipped_lines: usize,
pub(crate) label_filtered: bool,
pub(crate) excluded_by_label: Option<(Vec<(String, usize)>, usize)>,
}
pub(crate) fn active_filters_str(args: &SearchArgs) -> String {
let mut parts: Vec<String> = Vec::new();
for l in &args.labels {
parts.push(format!("-t {l}"));
}
for l in &args.labels_not {
parts.push(format!("-T {l}"));
}
if let Some(s) = &args.since {
parts.push(format!("--since {s}"));
}
if let Some(u) = &args.until {
parts.push(format!("--until {u}"));
}
if let Some(t) = &args.turn_range {
parts.push(format!("--turn {t}"));
}
if args.additional_context {
parts.push("--additional-context".to_string());
}
if parts.is_empty() {
"none".to_string()
} else {
parts.join(" ")
}
}
pub(crate) fn emit_empty_diagnosis(pattern: &str, diag: &EmptyDiagnosis) {
eprintln!(
"csift: 0 matches — a DEFINITIVE absence (exit 0), NOT an error. \
Scope: {} session(s). Active filters: {}.",
diag.sessions_in_scope, diag.active_filters
);
if diag.skipped_lines > 0 {
eprintln!(
"csift: caveat: {} — the absence is definitive for parseable lines only.",
crate::text::malformed_note(diag.skipped_lines)
);
}
let quoted = if pattern.is_empty() {
"the filter".to_string()
} else {
format!("\"{pattern}\"")
};
match &diag.excluded_by_label {
Some((rows, recs)) => {
let shown: Vec<String> = rows
.iter()
.take(6)
.map(|(l, n)| format!("{l} ×{n}"))
.collect();
let more = rows.len().saturating_sub(6);
let tail = if more > 0 {
format!(" (+{more} more label(s))")
} else {
String::new()
};
eprintln!(
"csift: ⚠ but {quoted} DOES occur — {recs} record(s) under: {}{tail}. \
Your -t/-T excluded them; drop -t/-T or select one of those labels.",
shown.join(" · ")
);
}
None if diag.label_filtered => {
eprintln!(
"csift: (even without the -t/-T filter, {quoted} has 0 matches here — \
genuinely absent in this scope, not a label mistake.)"
);
}
None => {
eprintln!(
"csift: to see what a scope holds before guessing a filter, run \
`csift search \"\" <target> --count-by label`."
);
}
}
}