lean_ctx/core/
rules_sections.rs1use super::tool_profiles::ToolProfile;
12
13fn has(p: &ToolProfile, name: &str) -> bool {
14 p.is_tool_enabled(name)
15}
16
17pub fn intent_section(p: &ToolProfile) -> String {
19 let mut lines = vec!["Tool selection by intent:".to_string()];
20
21 if has(p, "ctx_compose") {
22 lines.push("• Orient / understand code (call FIRST) -> ctx_compose".into());
23 }
24
25 let read_line = if has(p, "ctx_patch") {
26 "• Read a file -> ctx_read(path, mode=signatures|map|full); edit after reading -> ctx_patch"
27 } else {
28 "• Read a file -> ctx_read(path, mode=signatures|map|full)"
29 };
30 lines.push(read_line.into());
31
32 let mut search_parts = vec!["• Exact symbol -> ctx_search(action=symbol)"];
34 search_parts.push("pattern -> ctx_search");
35 search_parts.push("by meaning -> ctx_search(action=semantic)");
36 lines.push(search_parts.join("; "));
37
38 let mut nav = String::from("• Files by glob -> ctx_glob; structure -> ctx_tree");
39 if has(p, "ctx_callgraph") {
40 nav.push_str("; callers/impact -> ctx_callgraph");
41 }
42 lines.push(nav);
43
44 let mut verify = String::from("• Verify after edits -> ctx_shell(test/build)");
45 if has(p, "ctx_session") || has(p, "ctx_knowledge") {
46 verify.push_str("; memory -> ctx_session / ctx_knowledge");
47 }
48 lines.push(verify);
49
50 lines.push(
51 "Semantic questions -> search tools, not whole-file reads: \
52 reading more ≠ understanding more."
53 .into(),
54 );
55 lines.join("\n")
56}
57
58pub fn hook_covered_tools_section(p: &ToolProfile) -> String {
61 let mut lines = vec!["MANDATORY MAPPING (always use ctx_* instead of native):".to_string()];
62
63 lines.push("• Read/cat -> ctx_read(path, mode) — cached, 10 modes, re-reads ~13 tokens".into());
64 lines.push(
65 "• Grep/search -> ctx_search(pattern, path) — also action=symbol|semantic \
66 for definitions/meaning"
67 .into(),
68 );
69 lines.push("• Shell/bash -> ctx_shell(command) — 95+ compression patterns".into());
70
71 if has(p, "ctx_compose") {
72 lines.push(
73 "• ctx_compose — orient in code FIRST (bundles search + read + symbols) \
74 — call before editing/debugging"
75 .into(),
76 );
77 }
78
79 if has(p, "ctx_callgraph") {
80 lines.push(
81 "• ctx_callgraph — callers, callees, blast radius — use instead of manual \
82 file reading"
83 .into(),
84 );
85 }
86
87 if has(p, "ctx_session") || has(p, "ctx_knowledge") {
88 lines.push(
89 "• ctx_session / ctx_knowledge — persistent memory — record decisions & \
90 progress after milestones"
91 .into(),
92 );
93 }
94
95 if has(p, "ctx_expand") {
96 lines.push("• ctx_expand — recover full text from [Archived]/compressed output".into());
97 }
98
99 lines.join("\n")
100}
101
102pub fn shadow_minimal_section(p: &ToolProfile) -> String {
104 let mut exclusives = Vec::new();
105
106 if has(p, "ctx_compose") {
107 exclusives.push("ctx_compose (understand code, call first)");
108 }
109
110 exclusives.push("ctx_search(action=symbol) (exact symbol)");
112 exclusives.push("ctx_search(action=semantic) (by meaning)");
113
114 if has(p, "ctx_callgraph") {
115 exclusives.push("ctx_callgraph (callers)");
116 }
117 if has(p, "ctx_knowledge") || has(p, "ctx_session") {
118 exclusives.push("ctx_knowledge / ctx_session (memory)");
119 }
120
121 format!(
122 "lean-ctx shadow mode: native file/search/shell calls auto-route to ctx_* \
123 — no tool-mapping needed.\n\
124 Exclusive tools (no native trigger): {}.",
125 exclusives.join(", ")
126 )
127}
128
129pub fn anti_section(p: &ToolProfile) -> String {
131 let mut lines = vec!["Anti-patterns — do NOT:".to_string()];
132
133 if has(p, "ctx_compose") {
134 lines.push(
135 "• Chain ctx_search -> ctx_read -> ctx_search(action=symbol) \
136 — one ctx_compose replaces all three"
137 .into(),
138 );
139 }
140
141 lines.push("• Use ctx_read(mode=full) for orientation — use mode=signatures".into());
142
143 if has(p, "ctx_callgraph") || has(p, "ctx_graph") {
144 lines.push(
145 "• Use ctx_callgraph/ctx_graph for const/static/variable refs — they track \
146 call edges and file deps only; use ctx_search instead"
147 .into(),
148 );
149 }
150
151 lines.join("\n")
152}
153
154pub fn litm_end_section(p: &ToolProfile) -> String {
156 let mut prefs = Vec::new();
157
158 if has(p, "ctx_compose") {
159 prefs.push("ctx_compose>chain");
160 }
161 prefs.push("ctx_read>Read");
162 prefs.push("ctx_shell>Shell");
163 prefs.push("ctx_search>Grep");
164 prefs.push("ctx_glob>Glob");
165 prefs.push("ctx_tree>ls");
166
167 format!(
168 "TOOL PREFERENCE (END): {} | Edit/Write/Delete=native",
169 prefs.join(" ")
170 )
171}
172
173pub fn ctx_call_fallback(p: &ToolProfile) -> Option<String> {
176 if matches!(p, ToolProfile::Power) {
177 return None;
178 }
179 Some(
180 "Advanced tools not in your profile are available via ctx_call(tool=<name>) gateway."
181 .into(),
182 )
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn intent_minimal_omits_compose_and_callgraph() {
191 let text = intent_section(&ToolProfile::Minimal);
192 assert!(!text.contains("ctx_compose"), "minimal has no ctx_compose");
193 assert!(
194 !text.contains("ctx_callgraph"),
195 "minimal has no ctx_callgraph"
196 );
197 assert!(!text.contains("ctx_patch"), "minimal has no ctx_patch");
198 assert!(text.contains("ctx_read"), "minimal always has ctx_read");
199 assert!(text.contains("ctx_search"), "minimal always has ctx_search");
200 }
201
202 #[test]
203 fn intent_standard_includes_compose_and_callgraph() {
204 let text = intent_section(&ToolProfile::Standard);
205 assert!(text.contains("ctx_compose"), "standard has ctx_compose");
206 assert!(text.contains("ctx_callgraph"), "standard has ctx_callgraph");
207 assert!(text.contains("ctx_patch"), "standard has ctx_patch");
208 }
209
210 #[test]
211 fn intent_power_matches_full_set() {
212 let text = intent_section(&ToolProfile::Power);
213 assert!(text.contains("ctx_compose"));
214 assert!(text.contains("ctx_callgraph"));
215 assert!(text.contains("ctx_patch"));
216 assert!(text.contains("ctx_session"));
217 }
218
219 #[test]
220 fn intent_uses_folded_search_actions() {
221 for p in [
222 ToolProfile::Minimal,
223 ToolProfile::Standard,
224 ToolProfile::Power,
225 ] {
226 let text = intent_section(&p);
227 assert!(
228 !text.contains("ctx_symbol"),
229 "must use ctx_search(action=symbol), not ctx_symbol"
230 );
231 assert!(
232 !text.contains("ctx_semantic_search"),
233 "must use ctx_search(action=semantic), not ctx_semantic_search"
234 );
235 assert!(text.contains("ctx_search(action=symbol)"));
236 assert!(text.contains("ctx_search(action=semantic)"));
237 }
238 }
239
240 #[test]
241 fn hook_covered_tools_respects_profile() {
242 let min = hook_covered_tools_section(&ToolProfile::Minimal);
243 assert!(!min.contains("ctx_compose"), "minimal: no compose");
244 assert!(!min.contains("ctx_callgraph"), "minimal: no callgraph");
245 assert!(
246 min.contains("ctx_read"),
247 "minimal: always has ctx_read mapping"
248 );
249 assert!(
250 min.contains("ctx_search"),
251 "minimal: always has ctx_search mapping"
252 );
253 assert!(
254 min.contains("ctx_shell"),
255 "minimal: always has ctx_shell mapping"
256 );
257
258 let std = hook_covered_tools_section(&ToolProfile::Standard);
259 assert!(std.contains("ctx_compose"), "standard: compose present");
260 assert!(std.contains("ctx_callgraph"), "standard: callgraph present");
261 }
262
263 #[test]
264 fn shadow_minimal_respects_profile() {
265 let min = shadow_minimal_section(&ToolProfile::Minimal);
266 assert!(!min.contains("ctx_compose"));
267 assert!(!min.contains("ctx_callgraph"));
268 assert!(min.contains("ctx_search(action=symbol)"));
269 }
270
271 #[test]
272 fn anti_minimal_omits_compose_chain() {
273 let text = anti_section(&ToolProfile::Minimal);
274 assert!(
275 !text.contains("ctx_compose"),
276 "minimal: no compose anti-pattern"
277 );
278 assert!(
279 text.contains("ctx_read(mode=full)"),
280 "universal anti-pattern stays"
281 );
282 }
283
284 #[test]
285 fn ctx_call_fallback_absent_for_power() {
286 assert!(ctx_call_fallback(&ToolProfile::Power).is_none());
287 }
288
289 #[test]
290 fn ctx_call_fallback_present_for_non_power() {
291 assert!(ctx_call_fallback(&ToolProfile::Minimal).is_some());
292 assert!(ctx_call_fallback(&ToolProfile::Standard).is_some());
293 }
294
295 #[test]
296 fn litm_end_always_has_core_tools() {
297 for p in [
298 ToolProfile::Minimal,
299 ToolProfile::Standard,
300 ToolProfile::Power,
301 ] {
302 let text = litm_end_section(&p);
303 assert!(text.contains("ctx_read>Read"));
304 assert!(text.contains("ctx_shell>Shell"));
305 assert!(text.contains("ctx_search>Grep"));
306 }
307 }
308
309 #[test]
310 fn litm_end_compose_only_when_enabled() {
311 let min = litm_end_section(&ToolProfile::Minimal);
312 assert!(!min.contains("ctx_compose"));
313 let std = litm_end_section(&ToolProfile::Standard);
314 assert!(std.contains("ctx_compose"));
315 }
316}