Skip to main content

lean_ctx/core/
rules_canonical.rs

1//! Canonical Hybrid Policy — the single source of truth for all lean-ctx rules.
2//!
3//! Every template, injected rule file, SKILL.md, and MCP instructions field
4//! MUST derive its tool-mapping table from this module. No inline tool tables
5//! elsewhere in the codebase.
6
7/// Generates the canonical tool-mapping table for the given mode.
8pub fn tool_table(mode: Mode) -> &'static str {
9    match mode {
10        Mode::Hybrid => HYBRID_TABLE,
11        Mode::Mcp => MCP_TABLE,
12    }
13}
14
15/// Returns the full rules block (LITM-optimized) for dedicated rule files.
16pub 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-v11 -->
26
27CRITICAL: ALWAYS use lean-ctx tools instead of native equivalents. This is NOT optional.
28
29{table}
30
31{shell_note}
32
33## Workflow: Orient → Locate → Read → Edit → Verify → Record
341. Orient: ctx_overview(task) or ctx_compose(task, path)
352. Locate: ctx_search(pattern, path) or ctx_semantic_search(query)
363. Read: ctx_read(path, mode) — full before edits, signatures for context
374. Edit: ctx_edit(path, old_string, new_string) or native Edit
385. Verify: ctx_read(path, "diff") + ctx_shell("test command")
396. Record: ctx_knowledge(action="remember", content="...")
40
41File editing: use native Edit/StrReplace. Write, Delete, Glob → use normally.
42NEVER loop on Edit failures — switch to ctx_edit immediately.
43
44NEVER use native Read/Grep/Shell when ctx_* equivalents are available.
45<!-- /lean-ctx -->"#
46    )
47}
48
49/// Returns a short instructions string for MCP JSON `instructions` field.
50pub fn mcp_instructions(mode: Mode) -> &'static str {
51    match mode {
52        Mode::Hybrid => MCP_INSTRUCTIONS_HYBRID,
53        Mode::Mcp => MCP_INSTRUCTIONS_MCP,
54    }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Mode {
59    Hybrid,
60    Mcp,
61}
62
63impl Mode {
64    pub fn from_hook_mode(hook: &crate::hooks::HookMode) -> Self {
65        match hook {
66            crate::hooks::HookMode::Hybrid => Mode::Hybrid,
67            crate::hooks::HookMode::Mcp => Mode::Mcp,
68        }
69    }
70}
71
72const HYBRID_TABLE: &str = "\
73| MUST USE | NEVER USE | Why |
74|----------|-----------|-----|
75| `ctx_read(path, mode)` | `Read` / `cat` / `head` / `tail` | Cached, 10 read modes, re-reads ~13 tokens |
76| `ctx_search(pattern, path)` | `Grep` / `rg` | Compact, token-efficient results |
77| `lean-ctx -c \"<cmd>\"` (via Shell) | `ctx_shell` / raw `Shell` | CLI compression, no MCP overhead |
78| `lean-ctx ls [path]` (via Shell) | `ctx_tree` / `ls` / `find` | Compact directory maps |";
79
80const MCP_TABLE: &str = "\
81| MUST USE | NEVER USE | Why |
82|----------|-----------|-----|
83| `ctx_read(path, mode)` | `Read` / `cat` / `head` / `tail` | Cached, 10 read modes, re-reads ~13 tokens |
84| `ctx_search(pattern, path)` | `Grep` / `rg` | Compact, token-efficient results |
85| `ctx_shell(command)` | `Shell` / `bash` / terminal | Pattern compression for git/npm/cargo output |
86| `ctx_tree(path, depth)` | `ls` / `find` | Compact directory maps |";
87
88const MCP_INSTRUCTIONS_HYBRID: &str = "\
89lean-ctx tools replace Read/Grep/Shell/ls. Workflow: Orient(ctx_overview) → Locate(ctx_search) → Read(ctx_read) → Edit(ctx_edit/native) → Verify(ctx_read diff + lean-ctx -c test) → Record(ctx_knowledge). Edit/Write/Glob: native.";
90
91const MCP_INSTRUCTIONS_MCP: &str = "\
92lean-ctx tools replace Read/Grep/Shell/ls. Workflow: Orient(ctx_overview) → Locate(ctx_search) → Read(ctx_read) → Edit(ctx_edit/native) → Verify(ctx_read diff + ctx_shell test) → Record(ctx_knowledge). Edit/Write/Glob: native.";
93
94/// Tool-mapping in bullet format for MCP instructions blocks.
95pub fn tool_mapping_bullets(mode: Mode) -> &'static str {
96    match mode {
97        Mode::Hybrid => HYBRID_BULLETS,
98        Mode::Mcp => MCP_BULLETS,
99    }
100}
101
102// Bullets are deliberately minimal (#579): the MANDATORY header carries the
103// imperative once, and the LITM-END preference line repeats it at the end —
104// per-bullet "[NEVER ...]" tails were redundant token weight in every session.
105const MCP_BULLETS: &str = "\
106lean-ctx MCP — MANDATORY tool mapping:\n\
107• Read/cat/head/tail -> ctx_read(path, mode)\n\
108• Shell/bash -> ctx_shell(command)\n\
109• Grep/rg -> ctx_search(pattern, path)\n\
110• ls/find -> ctx_tree(path, depth)\n\
111• Edit/Write/Delete/Glob -> native (lean-ctx replaces READ only); if Edit fails, switch to ctx_edit(path, old, new) — never loop";
112
113const HYBRID_BULLETS: &str = "\
114lean-ctx — MANDATORY tool mapping:\n\
115• Read/cat/head/tail -> ctx_read(path, mode)\n\
116• Shell commands -> lean-ctx -c \"<cmd>\" (via Shell)  [NEVER ctx_shell]\n\
117• Grep/rg -> ctx_search(pattern, path)\n\
118• ls/find -> lean-ctx ls [path] (via Shell)\n\
119• Edit/Write/Delete/Glob -> native (lean-ctx replaces READ only); if Edit fails, switch to ctx_edit(path, old, new) — never loop";
120
121/// One line on purpose (#579): every word here rides in EVERY session's MCP
122/// instructions. Mode details live on disk (LEAN-CTX.md) and in tool schemas.
123pub fn ctx_read_modes_block() -> &'static str {
124    "ctx_read modes: auto(default)|full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M. Re-reads ~13 tok; fresh=true forces disk re-read."
125}
126
127/// One line on purpose (#579) — background automation needs awareness, not a
128/// manual. Long-form documentation lives in LEAN-CTX.md.
129pub fn automation_block() -> &'static str {
130    "Auto: preload/dedup/compress run in background. ctx_session=memory, ctx_knowledge=facts, ctx_semantic_search=meaning search, ctx_shell raw=true=uncompressed. Details: LEAN-CTX.md"
131}
132
133pub fn cep_block() -> &'static str {
134    "CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) 4.ONE LINE PER ACTION 5.QUALITY ANCHOR"
135}
136
137pub fn litm_end_block(mode: Mode) -> &'static str {
138    match mode {
139        Mode::Hybrid => {
140            "TOOL PREFERENCE (END): ctx_read>Read ctx_search>Grep lean-ctx_-c>Shell lean-ctx_ls>ls | Edit/Write/Glob=native"
141        }
142        Mode::Mcp => {
143            "TOOL PREFERENCE (END): ctx_read>Read ctx_shell>Shell ctx_search>Grep ctx_tree>ls | Edit/Write/Glob=native"
144        }
145    }
146}
147
148pub fn unified_tool_mode_block() -> &'static str {
149    "UNIFIED TOOL MODE (active):\n\
150     Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
151     See the ctx() tool description for available sub-tools."
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn hybrid_table_contains_must() {
160        assert!(HYBRID_TABLE.contains("MUST USE"));
161        assert!(!HYBRID_TABLE.contains("PREFER"));
162    }
163
164    #[test]
165    fn mcp_table_contains_must() {
166        assert!(MCP_TABLE.contains("MUST USE"));
167        assert!(!MCP_TABLE.contains("PREFER"));
168    }
169
170    #[test]
171    fn hybrid_table_uses_cli() {
172        assert!(HYBRID_TABLE.contains("lean-ctx -c"));
173        for line in HYBRID_TABLE.lines() {
174            assert!(
175                !line.starts_with("| `ctx_shell"),
176                "Hybrid table must not list ctx_shell in MUST USE column"
177            );
178        }
179    }
180
181    #[test]
182    fn mcp_table_uses_ctx_shell() {
183        assert!(MCP_TABLE.contains("ctx_shell"));
184        assert!(!MCP_TABLE.contains("lean-ctx -c"));
185    }
186
187    #[test]
188    fn dedicated_rules_have_markers() {
189        let rules = dedicated_rules(Mode::Hybrid);
190        assert!(rules.contains("lean-ctx-rules-v11"));
191        assert!(rules.contains("<!-- /lean-ctx -->"));
192    }
193
194    #[test]
195    fn dedicated_rules_litm_structure() {
196        for mode in [Mode::Hybrid, Mode::Mcp] {
197            let rules = dedicated_rules(mode);
198            let lines: Vec<&str> = rules.lines().collect();
199            let first_5 = lines[..5.min(lines.len())].join("\n");
200            assert!(
201                first_5.contains("CRITICAL") || first_5.contains("MUST"),
202                "LITM: MUST instruction near start for {mode:?}"
203            );
204            let last_3 = lines[lines.len().saturating_sub(3)..].join("\n");
205            assert!(
206                last_3.contains("MUST") || last_3.contains("NEVER"),
207                "LITM: reinforcement near end for {mode:?}"
208            );
209        }
210    }
211
212    #[test]
213    fn no_prefer_in_any_output() {
214        for mode in [Mode::Hybrid, Mode::Mcp] {
215            let rules = dedicated_rules(mode);
216            assert!(
217                !rules.contains("PREFER"),
218                "canonical rules must use MUST, not PREFER for {mode:?}"
219            );
220            let instructions = mcp_instructions(mode);
221            assert!(
222                !instructions.contains("PREFER"),
223                "MCP instructions must use MUST, not PREFER for {mode:?}"
224            );
225        }
226    }
227
228    #[test]
229    fn hybrid_bullets_use_cli() {
230        let bullets = tool_mapping_bullets(Mode::Hybrid);
231        for line in bullets.lines() {
232            if line.starts_with('•') {
233                assert!(
234                    !line.starts_with("• Shell/bash -> ctx_shell"),
235                    "Hybrid bullets must not map Shell to ctx_shell"
236                );
237            }
238        }
239        assert!(bullets.contains("lean-ctx -c"));
240    }
241
242    #[test]
243    fn mcp_bullets_no_lean_ctx_c() {
244        let bullets = tool_mapping_bullets(Mode::Mcp);
245        assert!(
246            !bullets.contains("lean-ctx -c"),
247            "MCP bullets must not reference lean-ctx -c"
248        );
249        assert!(bullets.contains("ctx_shell"));
250    }
251
252    #[test]
253    fn shared_sections_not_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
262    #[test]
263    fn bullets_carry_edit_failure_path() {
264        // The ctx_edit escape hatch is the one non-obvious compatibility rule;
265        // it must survive in the mapping bullets (#579 folded the old
266        // compatibility_block into them).
267        for mode in [Mode::Hybrid, Mode::Mcp] {
268            assert!(
269                tool_mapping_bullets(mode).contains("ctx_edit"),
270                "edit-failure path missing for {mode:?}"
271            );
272        }
273    }
274}