Skip to main content

lean_ctx/core/
instruction_compiler.rs

1use 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}