use super::tool_profiles::ToolProfile;
fn has(p: &ToolProfile, name: &str) -> bool {
p.is_tool_enabled(name)
}
fn memory_tools(p: &ToolProfile) -> Option<&'static str> {
match (has(p, "ctx_session"), has(p, "ctx_knowledge")) {
(true, true) => Some("ctx_session / ctx_knowledge"),
(true, false) => Some("ctx_session"),
(false, true) => Some("ctx_knowledge"),
(false, false) => None,
}
}
pub(crate) fn intent_section(p: &ToolProfile) -> String {
let mut lines = vec!["By task:".to_string()];
if has(p, "ctx_compose") {
lines.push("• Unfamiliar code -> ctx_compose (search + read + symbols in one call)".into());
}
let read_line = if has(p, "ctx_patch") {
"• Read -> ctx_read(mode=signatures|map|full); edit -> ctx_patch"
} else {
"• Read -> ctx_read(mode=signatures|map|full)"
};
lines.push(read_line.into());
lines.push(
"• Symbol -> ctx_search(action=symbol); meaning -> ctx_search(action=semantic)".into(),
);
let mut tail = Vec::new();
if has(p, "ctx_callgraph") {
tail.push("Callers/impact -> ctx_callgraph".to_string());
}
if let Some(memory) = memory_tools(p) {
tail.push(format!("memory -> {memory}"));
}
if !tail.is_empty() {
lines.push(format!("• {}", tail.join("; ")));
}
lines.join("\n")
}
pub(crate) fn hook_covered_tools_section(p: &ToolProfile) -> String {
let mut lines = vec!["lean-ctx tools:".to_string()];
lines.push("• Read/cat -> ctx_read(path, mode) — structural views, cached re-reads".into());
lines.push("• Grep/search -> ctx_search(pattern, path) — also action=symbol|semantic".into());
lines.push("• Shell/bash -> ctx_shell(command) — shaped command output".into());
if has(p, "ctx_compose") {
lines.push("• ctx_compose — search + read + symbols in one call".into());
}
if has(p, "ctx_callgraph") {
lines.push("• ctx_callgraph — callers, callees, blast radius".into());
}
if let Some(memory) = memory_tools(p) {
lines.push(format!("• {memory} — persistent memory"));
}
if has(p, "ctx_expand") {
lines.push("• ctx_expand — full text of [Archived]/compressed output".into());
}
lines.join("\n")
}
pub(crate) fn shadow_minimal_section(p: &ToolProfile) -> String {
let mut exclusives = Vec::new();
if has(p, "ctx_compose") {
exclusives.push("ctx_compose (search + read + symbols)");
}
exclusives.push("ctx_search(action=symbol) (exact symbol)");
exclusives.push("ctx_search(action=semantic) (by meaning)");
if has(p, "ctx_callgraph") {
exclusives.push("ctx_callgraph (callers)");
}
match (has(p, "ctx_session"), has(p, "ctx_knowledge")) {
(true, true) => exclusives.push("ctx_knowledge / ctx_session (memory)"),
(true, false) => exclusives.push("ctx_session (memory)"),
(false, true) => exclusives.push("ctx_knowledge (memory)"),
(false, false) => {}
}
let edit_line = if has(p, "ctx_patch") {
"File editing → the host's native edit tool (lean-ctx only handles reads); if denied, use ctx_patch.\n"
} else {
"File editing → the host's native edit tool (lean-ctx only handles reads).\n"
};
format!(
"lean-ctx shadow mode: native read/search/shell calls auto-route to ctx_* \
— no tool-mapping needed.\n\
{edit_line}\
Exclusive tools (no native trigger): {}.",
exclusives.join(", ")
)
}
pub(crate) fn anti_section(p: &ToolProfile) -> String {
let mut lines = vec!["Anti-patterns — do NOT:".to_string()];
if has(p, "ctx_compose") {
lines.push(
"• Chain ctx_search -> ctx_read -> ctx_search(action=symbol) \
— one ctx_compose replaces all three"
.into(),
);
}
lines.push("• Use ctx_read(mode=full) for orientation — use mode=signatures".into());
let graph_tools: Vec<&str> = ["ctx_callgraph", "ctx_graph"]
.into_iter()
.filter(|t| has(p, t))
.collect();
if !graph_tools.is_empty() {
let (names, verb) = match graph_tools.as_slice() {
[one] => (*one, "it tracks"),
_ => ("ctx_callgraph/ctx_graph", "they track"),
};
lines.push(format!(
"• Use {names} for const/static/variable refs — {verb} \
call edges and file deps only; use ctx_search instead"
));
}
lines.join("\n")
}
pub(crate) fn must_invoke_section(_p: &ToolProfile) -> String {
super::rules_canonical::MUST_INVOKE.to_string()
}
pub(crate) fn parallel_section(p: &ToolProfile) -> String {
if has(p, "ctx_compose") {
super::rules_canonical::PARALLEL.to_string()
} else {
"PARALLEL: fire independent tool calls in the SAME turn.".to_string()
}
}
pub(crate) fn litm_end_section(p: &ToolProfile) -> String {
let mut prefs = Vec::new();
if has(p, "ctx_compose") {
prefs.push("ctx_compose>chain");
}
prefs.push("ctx_read>Read");
prefs.push("ctx_shell>Shell");
prefs.push("ctx_search>Grep");
prefs.push("ctx_glob>Glob");
prefs.push("ctx_tree>ls");
format!(
"TOOL PREFERENCE (END): {} | Edit/Write/Delete=native",
prefs.join(" ")
)
}
pub(crate) fn ctx_call_fallback(p: &ToolProfile) -> Option<String> {
if matches!(p, ToolProfile::Power) {
return None;
}
Some("Other tools: ctx_call(tool=<name>).".into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intent_minimal_omits_compose_and_callgraph() {
let text = intent_section(&ToolProfile::Minimal);
assert!(!text.contains("ctx_compose"), "minimal has no ctx_compose");
assert!(
!text.contains("ctx_callgraph"),
"minimal has no ctx_callgraph"
);
assert!(!text.contains("ctx_patch"), "minimal has no ctx_patch");
assert!(text.contains("ctx_read"), "minimal always has ctx_read");
assert!(text.contains("ctx_search"), "minimal always has ctx_search");
}
#[test]
fn intent_standard_includes_compose_and_callgraph() {
let text = intent_section(&ToolProfile::Standard);
assert!(text.contains("ctx_compose"), "standard has ctx_compose");
assert!(text.contains("ctx_callgraph"), "standard has ctx_callgraph");
assert!(text.contains("ctx_patch"), "standard has ctx_patch");
}
#[test]
fn intent_power_matches_full_set() {
let text = intent_section(&ToolProfile::Power);
assert!(text.contains("ctx_compose"));
assert!(text.contains("ctx_callgraph"));
assert!(text.contains("ctx_patch"));
assert!(text.contains("ctx_session"));
}
#[test]
fn intent_uses_folded_search_actions() {
for p in [
ToolProfile::Minimal,
ToolProfile::Standard,
ToolProfile::Power,
] {
let text = intent_section(&p);
assert!(
!text.contains("ctx_symbol"),
"must use ctx_search(action=symbol), not ctx_symbol"
);
assert!(
!text.contains("ctx_semantic_search"),
"must use ctx_search(action=semantic), not ctx_semantic_search"
);
assert!(text.contains("ctx_search(action=symbol)"));
assert!(text.contains("ctx_search(action=semantic)"));
}
}
#[test]
fn hook_covered_tools_respects_profile() {
let min = hook_covered_tools_section(&ToolProfile::Minimal);
assert!(!min.contains("ctx_compose"), "minimal: no compose");
assert!(!min.contains("ctx_callgraph"), "minimal: no callgraph");
assert!(
min.contains("ctx_read"),
"minimal: always has ctx_read mapping"
);
assert!(
min.contains("ctx_search"),
"minimal: always has ctx_search mapping"
);
assert!(
min.contains("ctx_shell"),
"minimal: always has ctx_shell mapping"
);
let std = hook_covered_tools_section(&ToolProfile::Standard);
assert!(std.contains("ctx_compose"), "standard: compose present");
assert!(std.contains("ctx_callgraph"), "standard: callgraph present");
}
#[test]
fn shadow_minimal_respects_profile() {
let min = shadow_minimal_section(&ToolProfile::Minimal);
assert!(!min.contains("ctx_compose"));
assert!(!min.contains("ctx_callgraph"));
assert!(min.contains("ctx_search(action=symbol)"));
assert!(!min.contains("ctx_patch"));
let power = shadow_minimal_section(&ToolProfile::Power);
assert!(power.contains("ctx_patch"));
}
#[test]
fn nudges_name_ctx_compose_only_when_the_profile_has_it() {
use crate::core::rules_canonical::{MUST_INVOKE, PARALLEL};
assert_eq!(parallel_section(&ToolProfile::Power), PARALLEL);
let minimal = parallel_section(&ToolProfile::Minimal);
assert!(minimal.starts_with("PARALLEL"));
assert!(!minimal.contains("ctx_compose"), "{minimal}");
for p in [ToolProfile::Minimal, ToolProfile::Power] {
assert_eq!(must_invoke_section(&p), MUST_INVOKE);
}
assert!(!MUST_INVOKE.contains("ctx_compose"));
}
#[test]
fn anti_graph_line_names_only_enabled_graph_tools() {
let graph_only = ToolProfile::Custom(vec!["ctx_read".into(), "ctx_graph".into()]);
let text = anti_section(&graph_only);
assert!(text.contains("• Use ctx_graph for const/static/variable refs — it tracks"));
assert!(!text.contains("ctx_callgraph"), "{text}");
let power = anti_section(&ToolProfile::Power);
assert!(
power.contains("ctx_callgraph/ctx_graph for const/static/variable refs — they track")
);
}
#[test]
fn shadow_edit_line_names_no_host_tool() {
for p in [
ToolProfile::Minimal,
ToolProfile::Standard,
ToolProfile::Power,
] {
let text = shadow_minimal_section(&p);
assert!(text.contains("the host's native edit tool"), "{p}");
for host_tool in ["StrReplace", "Edit/", "apply_patch"] {
assert!(!text.contains(host_tool), "{p}: names {host_tool}");
}
}
}
#[test]
fn anti_minimal_omits_compose_chain() {
let text = anti_section(&ToolProfile::Minimal);
assert!(
!text.contains("ctx_compose"),
"minimal: no compose anti-pattern"
);
assert!(
text.contains("ctx_read(mode=full)"),
"universal anti-pattern stays"
);
}
#[test]
fn ctx_call_fallback_absent_for_power() {
assert!(ctx_call_fallback(&ToolProfile::Power).is_none());
}
#[test]
fn ctx_call_fallback_present_for_non_power() {
assert!(ctx_call_fallback(&ToolProfile::Minimal).is_some());
assert!(ctx_call_fallback(&ToolProfile::Standard).is_some());
}
#[test]
fn litm_end_always_has_core_tools() {
for p in [
ToolProfile::Minimal,
ToolProfile::Standard,
ToolProfile::Power,
] {
let text = litm_end_section(&p);
assert!(text.contains("ctx_read>Read"));
assert!(text.contains("ctx_shell>Shell"));
assert!(text.contains("ctx_search>Grep"));
}
}
#[test]
fn litm_end_compose_only_when_enabled() {
let min = litm_end_section(&ToolProfile::Minimal);
assert!(!min.contains("ctx_compose"));
let std = litm_end_section(&ToolProfile::Standard);
assert!(std.contains("ctx_compose"));
}
}