lean_ctx/core/
rules_canonical.rs1pub fn tool_table(mode: Mode) -> &'static str {
9 match mode {
10 Mode::Hybrid => HYBRID_TABLE,
11 Mode::Mcp => MCP_TABLE,
12 }
13}
14
15pub fn dedicated_rules(mode: Mode) -> String {
17 let table = tool_table(mode);
18 let shell_note = match mode {
19 Mode::Hybrid => "Shell commands: use `lean-ctx -c \"<cmd>\"` via your Shell tool. NEVER use `ctx_shell` in Hybrid mode.",
20 Mode::Mcp => "Shell commands: use `ctx_shell(command)`. NEVER use raw Shell/bash.",
21 };
22
23 format!(
24 r"# lean-ctx — Context Engineering Layer
25<!-- lean-ctx-rules-v10 -->
26
27CRITICAL: ALWAYS use lean-ctx tools instead of native equivalents. This is NOT optional.
28
29{table}
30
31{shell_note}
32
33File editing: use native Edit/StrReplace. Write, Delete, Glob → use normally.
34NEVER loop on Edit failures — switch to ctx_edit immediately.
35
36Fallback only if a lean-ctx tool is unavailable: use native equivalents.
37REMINDER: You MUST use lean-ctx tools. NEVER use native Read, Grep, or Shell directly.
38<!-- /lean-ctx -->"
39 )
40}
41
42pub fn mcp_instructions(mode: Mode) -> &'static str {
44 match mode {
45 Mode::Hybrid => MCP_INSTRUCTIONS_HYBRID,
46 Mode::Mcp => MCP_INSTRUCTIONS_MCP,
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Mode {
52 Hybrid,
53 Mcp,
54}
55
56impl Mode {
57 pub fn from_hook_mode(hook: &crate::hooks::HookMode) -> Self {
58 match hook {
59 crate::hooks::HookMode::Hybrid => Mode::Hybrid,
60 crate::hooks::HookMode::Mcp => Mode::Mcp,
61 }
62 }
63}
64
65const HYBRID_TABLE: &str = "\
66| MUST USE | NEVER USE | Why |
67|----------|-----------|-----|
68| `ctx_read(path, mode)` | `Read` / `cat` / `head` / `tail` | Cached, 10 read modes, re-reads ~13 tokens |
69| `ctx_search(pattern, path)` | `Grep` / `rg` | Compact, token-efficient results |
70| `lean-ctx -c \"<cmd>\"` (via Shell) | `ctx_shell` / raw `Shell` | CLI compression, no MCP overhead |
71| `lean-ctx ls [path]` (via Shell) | `ctx_tree` / `ls` / `find` | Compact directory maps |";
72
73const MCP_TABLE: &str = "\
74| MUST USE | NEVER USE | Why |
75|----------|-----------|-----|
76| `ctx_read(path, mode)` | `Read` / `cat` / `head` / `tail` | Cached, 10 read modes, re-reads ~13 tokens |
77| `ctx_search(pattern, path)` | `Grep` / `rg` | Compact, token-efficient results |
78| `ctx_shell(command)` | `Shell` / `bash` / terminal | Pattern compression for git/npm/cargo output |
79| `ctx_tree(path, depth)` | `ls` / `find` | Compact directory maps |";
80
81const MCP_INSTRUCTIONS_HYBRID: &str = "\
82lean-ctx tools replace Read/Grep/Shell/ls. See tool descriptions for details. Edit/Write/Glob: native.";
83
84const MCP_INSTRUCTIONS_MCP: &str = "\
85lean-ctx tools replace Read/Grep/Shell/ls. See tool descriptions for details. Edit/Write/Glob: native.";
86
87pub fn tool_mapping_bullets(mode: Mode) -> &'static str {
89 match mode {
90 Mode::Hybrid => HYBRID_BULLETS,
91 Mode::Mcp => MCP_BULLETS,
92 }
93}
94
95const MCP_BULLETS: &str = "\
96lean-ctx MCP — MANDATORY tool mapping:\n\
97• Read/cat/head/tail -> ctx_read(path, mode) [NEVER use native Read]\n\
98• Shell/bash -> ctx_shell(command) [NEVER use native Shell]\n\
99• Grep/rg -> ctx_search(pattern, path) [NEVER use native Grep]\n\
100• ls/find -> ctx_tree(path, depth)\n\
101• Edit/StrReplace -> use native (lean-ctx only replaces READ, not WRITE)\n\
102• Write, Delete, Glob -> use normally";
103
104const HYBRID_BULLETS: &str = "\
105lean-ctx — MANDATORY tool mapping:\n\
106• Read/cat/head/tail -> ctx_read(path, mode) [NEVER use native Read]\n\
107• Shell commands -> lean-ctx -c \"<cmd>\" (via Shell) [NEVER use ctx_shell]\n\
108• Grep/rg -> ctx_search(pattern, path) [NEVER use native Grep]\n\
109• ls/find -> lean-ctx ls [path] (via Shell)\n\
110• Edit/StrReplace -> use native (lean-ctx only replaces READ, not WRITE)\n\
111• Write, Delete, Glob -> use normally";
112
113pub fn compatibility_block() -> &'static str {
114 "COMPATIBILITY: lean-ctx replaces READ operations only. Edit/Write/StrReplace stay native.\n\
115 FILE EDITING: Native Edit/StrReplace preferred. If Edit fails, use ctx_edit immediately."
116}
117
118pub fn ctx_read_modes_block() -> &'static str {
119 "ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M. Auto-selects. Cached re-reads can be ~13 tok when unchanged. Fn refs F1,F2.. persist.\n\
120 Cache auto-validates via file mtime. Use fresh=true (or start_line / lines:N-M) to force a disk re-read."
121}
122
123pub fn automation_block() -> &'static str {
124 "Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress run behind the scenes. Checkpoint every 15 calls.\n\
125 Multi-agent: ctx_agent(action=handoff|sync). Diary: ctx_agent(action=diary, category=discovery|decision|blocker|progress|insight).\n\
126 ctx_semantic_search for meaning-based search. ctx_session for memory. ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.\n\
127 ctx_shell raw=true for uncompressed output."
128}
129
130pub fn cep_block() -> &'static str {
131 "CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) 4.ONE LINE PER ACTION 5.QUALITY ANCHOR"
132}
133
134pub fn litm_end_block(mode: Mode) -> &'static str {
135 match mode {
136 Mode::Hybrid => {
137 "--- TOOL PREFERENCE (LITM-END) ---\n\
138 ctx_read>Read ctx_search>Grep lean-ctx_-c>Shell lean-ctx_ls>ls | Edit/Write/Glob=native"
139 }
140 Mode::Mcp => {
141 "--- TOOL PREFERENCE (LITM-END) ---\n\
142 ctx_read>Read ctx_shell>Shell ctx_search>Grep ctx_tree>ls | Edit/Write/Glob=native"
143 }
144 }
145}
146
147pub fn unified_tool_mode_block() -> &'static str {
148 "UNIFIED TOOL MODE (active):\n\
149 Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
150 See the ctx() tool description for available sub-tools."
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn hybrid_table_contains_must() {
159 assert!(HYBRID_TABLE.contains("MUST USE"));
160 assert!(!HYBRID_TABLE.contains("PREFER"));
161 }
162
163 #[test]
164 fn mcp_table_contains_must() {
165 assert!(MCP_TABLE.contains("MUST USE"));
166 assert!(!MCP_TABLE.contains("PREFER"));
167 }
168
169 #[test]
170 fn hybrid_table_uses_cli() {
171 assert!(HYBRID_TABLE.contains("lean-ctx -c"));
172 for line in HYBRID_TABLE.lines() {
173 assert!(
174 !line.starts_with("| `ctx_shell"),
175 "Hybrid table must not list ctx_shell in MUST USE column"
176 );
177 }
178 }
179
180 #[test]
181 fn mcp_table_uses_ctx_shell() {
182 assert!(MCP_TABLE.contains("ctx_shell"));
183 assert!(!MCP_TABLE.contains("lean-ctx -c"));
184 }
185
186 #[test]
187 fn dedicated_rules_have_markers() {
188 let rules = dedicated_rules(Mode::Hybrid);
189 assert!(rules.contains("lean-ctx-rules-v10"));
190 assert!(rules.contains("<!-- /lean-ctx -->"));
191 }
192
193 #[test]
194 fn dedicated_rules_litm_structure() {
195 for mode in [Mode::Hybrid, Mode::Mcp] {
196 let rules = dedicated_rules(mode);
197 let lines: Vec<&str> = rules.lines().collect();
198 let first_5 = lines[..5.min(lines.len())].join("\n");
199 assert!(
200 first_5.contains("CRITICAL") || first_5.contains("MUST"),
201 "LITM: MUST instruction near start for {mode:?}"
202 );
203 let last_3 = lines[lines.len().saturating_sub(3)..].join("\n");
204 assert!(
205 last_3.contains("MUST") || last_3.contains("NEVER"),
206 "LITM: reinforcement near end for {mode:?}"
207 );
208 }
209 }
210
211 #[test]
212 fn no_prefer_in_any_output() {
213 for mode in [Mode::Hybrid, Mode::Mcp] {
214 let rules = dedicated_rules(mode);
215 assert!(
216 !rules.contains("PREFER"),
217 "canonical rules must use MUST, not PREFER for {mode:?}"
218 );
219 let instructions = mcp_instructions(mode);
220 assert!(
221 !instructions.contains("PREFER"),
222 "MCP instructions must use MUST, not PREFER for {mode:?}"
223 );
224 }
225 }
226
227 #[test]
228 fn hybrid_bullets_use_cli() {
229 let bullets = tool_mapping_bullets(Mode::Hybrid);
230 for line in bullets.lines() {
231 if line.starts_with('•') {
232 assert!(
233 !line.starts_with("• Shell/bash -> ctx_shell"),
234 "Hybrid bullets must not map Shell to ctx_shell"
235 );
236 }
237 }
238 assert!(bullets.contains("lean-ctx -c"));
239 }
240
241 #[test]
242 fn mcp_bullets_no_lean_ctx_c() {
243 let bullets = tool_mapping_bullets(Mode::Mcp);
244 assert!(
245 !bullets.contains("lean-ctx -c"),
246 "MCP bullets must not reference lean-ctx -c"
247 );
248 assert!(bullets.contains("ctx_shell"));
249 }
250
251 #[test]
252 fn shared_sections_not_empty() {
253 assert!(!compatibility_block().is_empty());
254 assert!(!ctx_read_modes_block().is_empty());
255 assert!(!automation_block().is_empty());
256 assert!(!cep_block().is_empty());
257 assert!(!litm_end_block(Mode::Mcp).is_empty());
258 assert!(!litm_end_block(Mode::Hybrid).is_empty());
259 assert!(!unified_tool_mode_block().is_empty());
260 }
261}