use super::protocol::JsonRpcError;
use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Tier {
L0,
#[default]
L1,
L2,
}
impl Tier {
pub fn take_from(args: &mut Value) -> Result<Self, JsonRpcError> {
let Some(raw) = args
.as_object_mut()
.and_then(|object| object.remove("tier"))
else {
return Ok(Tier::default());
};
let text = match &raw {
Value::String(text) => text.trim().to_ascii_lowercase(),
Value::Number(number) => format!("l{number}"),
Value::Null => return Ok(Tier::default()),
_ => String::new(),
};
match text.as_str() {
"l0" => Ok(Tier::L0),
"l1" => Ok(Tier::L1),
"l2" => Ok(Tier::L2),
_ => Err(JsonRpcError::invalid_params_with_suggestion(
format!("Invalid tier {raw}"),
"Use tier: \"l0\" (identity card), \"l1\" (bounded overview, default) or \"l2\" (full detail)"
.to_string(),
)),
}
}
}
const NAME_KEYS: [&str; 8] = [
"name",
"symbol",
"symbol_name",
"qualified_name",
"file",
"file_path",
"path",
"id",
];
const MAX_SCALAR_CHARS: usize = 100;
const MAX_HEADLINES: usize = 8;
const MAX_TOP: usize = 5;
fn short(text: &str) -> String {
if text.chars().count() <= MAX_SCALAR_CHARS {
return text.to_string();
}
let cut: String = text.chars().take(MAX_SCALAR_CHARS - 1).collect();
format!("{cut}…")
}
fn label(item: &Value) -> Option<String> {
let object = item.as_object()?;
let name = NAME_KEYS
.iter()
.find_map(|key| object.get(*key).and_then(Value::as_str));
let name = name?;
let place = ["file_path", "file", "path"]
.iter()
.find_map(|key| object.get(*key).and_then(Value::as_str))
.filter(|place| *place != name);
Some(match place {
Some(place) => format!("{} ({})", short(name), short(place)),
None => short(name),
})
}
fn scalar(value: &Value) -> Option<String> {
match value {
Value::String(text) if !text.is_empty() => Some(short(text)),
Value::Number(number) => Some(number.to_string()),
Value::Bool(flag) => Some(flag.to_string()),
_ => None,
}
}
fn headlines(object: &Map<String, Value>, prefix: &str, out: &mut Vec<String>) {
let priority = |key: &str| {
!(key.starts_with("total") || key == "count" || key == "returned" || key == "has_more")
};
let mut entries: Vec<(&String, &Value)> = object.iter().collect();
entries.sort_by_key(|(key, _)| priority(key));
for (key, value) in entries {
if out.len() >= MAX_HEADLINES {
return;
}
if key.starts_with('_') || key == "content" || key == "patch" || key == "source" {
continue;
}
let name = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
match value {
Value::Array(items) => out.push(format!("{name}={}", items.len())),
Value::Object(inner)
if prefix.is_empty()
&& matches!(
key.as_str(),
"summary" | "stats" | "impact_summary" | "retrieval"
) =>
{
headlines(inner, key, out);
}
other => {
if let Some(text) = scalar(other) {
out.push(format!("{name}={text}"));
}
}
}
}
}
pub fn identity_card(tool: &str, value: &Value) -> String {
let mut lines = Vec::new();
let mut facts = Vec::new();
if let Some(object) = value.as_object() {
headlines(object, "", &mut facts);
let top = object.values().find_map(|candidate| {
let items = candidate.as_array()?;
let labels: Vec<String> = items.iter().filter_map(label).take(MAX_TOP).collect();
(!labels.is_empty()).then_some(labels)
});
lines.push(format!(
"{tool} · {}",
if facts.is_empty() {
"ok".to_string()
} else {
facts.join(" ")
}
));
if let Some(top) = top {
lines.push(format!("top: {}", top.join(", ")));
}
} else if let Some(items) = value.as_array() {
lines.push(format!("{tool} · items={}", items.len()));
let labels: Vec<String> = items.iter().filter_map(label).take(MAX_TOP).collect();
if !labels.is_empty() {
lines.push(format!("top: {}", labels.join(", ")));
}
} else {
lines.push(format!(
"{tool} · {}",
scalar(value).unwrap_or_else(|| "ok".to_string())
));
}
lines.push(
"(tier l0 — pass tier=\"l1\" for the overview or \"l2\" for full detail)".to_string(),
);
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_tier_defaults_to_l1_and_is_consumed() {
let mut args = json!({"query": "x"});
assert_eq!(Tier::take_from(&mut args).unwrap(), Tier::L1);
let mut args = json!({"query": "x", "tier": "L2"});
assert_eq!(Tier::take_from(&mut args).unwrap(), Tier::L2);
assert_eq!(args, json!({"query": "x"}));
let mut args = json!({"tier": 0});
assert_eq!(Tier::take_from(&mut args).unwrap(), Tier::L0);
}
#[test]
fn test_invalid_tier_is_rejected_with_guidance() {
let mut args = json!({"tier": "l9"});
let error = Tier::take_from(&mut args).unwrap_err();
assert!(error.message_with_hint().contains("l0"), "{error}");
}
#[test]
fn test_identity_card_has_headlines_and_top_hits() {
let value = json!({
"count": 3,
"has_more": true,
"results": [
{"symbol_name": "alpha", "file_path": "src/a.rs", "score": {"overall": 0.9}},
{"symbol_name": "beta", "file_path": "src/b.rs"},
],
"_meta": {"noise": 1},
});
let card = identity_card("leindex_search", &value);
assert!(card.contains("count=3"), "{card}");
assert!(card.contains("results=2"), "{card}");
assert!(card.contains("alpha (src/a.rs), beta (src/b.rs)"), "{card}");
assert!(!card.contains('?'), "no placeholder glyphs: {card}");
assert!(!card.contains("noise"), "{card}");
assert!(card.lines().count() <= 4);
}
#[test]
fn test_identity_card_leads_with_totals_even_when_keys_sort_late() {
let value = json!({
"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5, "a6": 6, "a7": 7, "a8": 8, "a9": 9,
"total_matches": 42, "has_more": true,
});
let card = identity_card("t", &value);
assert!(
card.contains("total_matches=42") && card.contains("has_more=true"),
"{card}"
);
}
#[test]
fn test_identity_card_is_small_for_huge_results() {
let results: Vec<Value> = (0..5_000)
.map(|i| json!({"name": format!("symbol_{i}"), "file": "f.rs"}))
.collect();
let card = identity_card("t", &json!({"results": results, "total": 5000}));
assert!(card.len() < 500, "{} bytes", card.len());
}
}