lean_ctx/core/
instruction_compiler.rs1use crate::core::client_constraints;
2use crate::core::profiles;
3use crate::core::protocol::CrpMode;
4
5#[derive(Debug, Clone, serde::Serialize)]
6#[serde(rename_all = "camelCase")]
7pub struct CompiledRuleFile {
8 pub path: String,
9 pub content: String,
10}
11
12#[derive(Debug, Clone, serde::Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct CompiledInstructions {
15 pub schema_version: u32,
16 pub client: String,
17 pub profile: String,
18 pub crp_mode: String,
19 pub unified_tool_mode: bool,
20 pub mcp_instructions: String,
21 pub rules_files: Vec<CompiledRuleFile>,
22}
23
24#[derive(Debug, Clone, Copy, Default)]
25pub struct CompileOptions {
26 pub unified: bool,
27 pub include_rules_files: bool,
28 pub crp_mode_override: Option<CrpMode>,
29}
30
31pub fn compile(
32 client_id: &str,
33 profile_name: &str,
34 opts: CompileOptions,
35) -> Result<CompiledInstructions, String> {
36 let client_id = client_id.trim();
37 let profile_name = profile_name.trim();
38 if client_id.is_empty() {
39 return Err("missing client id".to_string());
40 }
41 if profile_name.is_empty() {
42 return Err("missing profile name".to_string());
43 }
44
45 let constraints = client_constraints::by_client_id(client_id);
46 if constraints.is_none() {
47 return Err(format!(
48 "unknown client '{client_id}' (use 'lean-ctx instructions --list-clients')"
49 ));
50 }
51
52 let profile = profiles::load_profile(profile_name)
53 .ok_or_else(|| format!("unknown profile '{profile_name}'"))?;
54
55 let crp_mode = opts
56 .crp_mode_override
57 .or_else(|| CrpMode::parse(profile.compression.crp_mode_effective()))
58 .unwrap_or(CrpMode::Tdd);
59
60 let mcp_instructions = crate::instructions::build_instructions_with_client_for_compiler(
61 crp_mode,
62 client_id,
63 opts.unified,
64 );
65
66 if let Some(cap) = constraints.and_then(|c| c.mcp_instructions_max_chars) {
67 if mcp_instructions.len() > cap {
68 return Err(format!(
69 "compiled MCP instructions exceed cap for {client_id}: {} > {cap}",
70 mcp_instructions.len()
71 ));
72 }
73 }
74
75 let mut rules_files = Vec::new();
76 if opts.include_rules_files && client_id == "claude-code" {
77 let config_dir = crate::instructions::claude_config_dir_display();
78 rules_files.push(CompiledRuleFile {
79 path: format!("{config_dir}/rules/lean-ctx.md"),
80 content: crate::rules_inject::rules_dedicated_markdown().to_string(),
81 });
82 }
83
84 Ok(CompiledInstructions {
85 schema_version: 1,
86 client: client_id.to_string(),
87 profile: profile.profile.name,
88 crp_mode: match crp_mode {
89 CrpMode::Off => "off",
90 CrpMode::Compact => "compact",
91 CrpMode::Tdd => "tdd",
92 }
93 .to_string(),
94 unified_tool_mode: opts.unified,
95 mcp_instructions,
96 rules_files,
97 })
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn compiled_instructions_are_deterministic() {
106 let a = compile(
107 "cursor",
108 "exploration",
109 CompileOptions {
110 unified: false,
111 include_rules_files: false,
112 crp_mode_override: Some(CrpMode::Tdd),
113 },
114 )
115 .unwrap();
116 let b = compile(
117 "cursor",
118 "exploration",
119 CompileOptions {
120 unified: false,
121 include_rules_files: false,
122 crp_mode_override: Some(CrpMode::Tdd),
123 },
124 )
125 .unwrap();
126 assert_eq!(a.mcp_instructions, b.mcp_instructions);
127 }
128
129 #[test]
130 fn compiles_for_all_known_clients() {
131 for c in crate::core::client_constraints::ALL_CLIENTS {
132 let out = compile(
133 c.id,
134 "exploration",
135 CompileOptions {
136 unified: false,
137 include_rules_files: false,
138 crp_mode_override: Some(CrpMode::Tdd),
139 },
140 )
141 .unwrap();
142 assert!(
143 !out.mcp_instructions.trim().is_empty(),
144 "empty instructions for client {}",
145 c.id
146 );
147 }
148 }
149
150 #[test]
151 fn claude_mcp_instructions_respect_cap() {
152 let out = compile(
153 "claude-code",
154 "exploration",
155 CompileOptions {
156 unified: false,
157 include_rules_files: false,
158 crp_mode_override: Some(CrpMode::Tdd),
159 },
160 )
161 .unwrap();
162 assert!(out.mcp_instructions.len() <= 2048);
163 }
164}