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-v9 -->
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
42/// Returns a short instructions string for MCP JSON `instructions` field.
43pub 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 = "\
82MUST use ctx_read instead of Read/cat. MUST use ctx_search instead of Grep/rg. \
83MUST use `lean-ctx -c` via Shell instead of ctx_shell or raw Shell. \
84Edit/Write/Delete/Glob: use native tools normally.";
85
86const MCP_INSTRUCTIONS_MCP: &str = "\
87MUST use ctx_read instead of Read/cat. MUST use ctx_search instead of Grep/rg. \
88MUST use ctx_shell instead of Shell/bash. MUST use ctx_tree instead of ls/find. \
89Edit/Write/Delete/Glob: use native tools normally.";
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn hybrid_table_contains_must() {
97        assert!(HYBRID_TABLE.contains("MUST USE"));
98        assert!(!HYBRID_TABLE.contains("PREFER"));
99    }
100
101    #[test]
102    fn mcp_table_contains_must() {
103        assert!(MCP_TABLE.contains("MUST USE"));
104        assert!(!MCP_TABLE.contains("PREFER"));
105    }
106
107    #[test]
108    fn hybrid_table_uses_cli() {
109        assert!(HYBRID_TABLE.contains("lean-ctx -c"));
110        for line in HYBRID_TABLE.lines() {
111            assert!(
112                !line.starts_with("| `ctx_shell"),
113                "Hybrid table must not list ctx_shell in MUST USE column"
114            );
115        }
116    }
117
118    #[test]
119    fn mcp_table_uses_ctx_shell() {
120        assert!(MCP_TABLE.contains("ctx_shell"));
121        assert!(!MCP_TABLE.contains("lean-ctx -c"));
122    }
123
124    #[test]
125    fn dedicated_rules_have_markers() {
126        let rules = dedicated_rules(Mode::Hybrid);
127        assert!(rules.contains("lean-ctx-rules-v9"));
128        assert!(rules.contains("<!-- /lean-ctx -->"));
129    }
130
131    #[test]
132    fn dedicated_rules_litm_structure() {
133        for mode in [Mode::Hybrid, Mode::Mcp] {
134            let rules = dedicated_rules(mode);
135            let lines: Vec<&str> = rules.lines().collect();
136            let first_5 = lines[..5.min(lines.len())].join("\n");
137            assert!(
138                first_5.contains("CRITICAL") || first_5.contains("MUST"),
139                "LITM: MUST instruction near start for {mode:?}"
140            );
141            let last_3 = lines[lines.len().saturating_sub(3)..].join("\n");
142            assert!(
143                last_3.contains("MUST") || last_3.contains("NEVER"),
144                "LITM: reinforcement near end for {mode:?}"
145            );
146        }
147    }
148
149    #[test]
150    fn no_prefer_in_any_output() {
151        for mode in [Mode::Hybrid, Mode::Mcp] {
152            let rules = dedicated_rules(mode);
153            assert!(
154                !rules.contains("PREFER"),
155                "canonical rules must use MUST, not PREFER for {mode:?}"
156            );
157            let instructions = mcp_instructions(mode);
158            assert!(
159                !instructions.contains("PREFER"),
160                "MCP instructions must use MUST, not PREFER for {mode:?}"
161            );
162        }
163    }
164}