Skip to main content

lean_ctx/core/
task_briefing.rs

1use crate::core::intent_engine::{classify, TaskClassification, TaskType};
2
3#[derive(Debug)]
4pub struct TaskBriefing {
5    pub classification: TaskClassification,
6    pub completeness_signal: CompletenessSignal,
7    pub output_instruction: &'static str,
8    pub context_hints: Vec<String>,
9    /// Lab-only: thinking instruction for direct LLM API calls.
10    /// NEVER inject into MCP tool outputs — would override user's model thinking behavior.
11    pub lab_thinking_instruction: &'static str,
12}
13
14#[derive(Debug, Clone, Copy)]
15pub enum CompletenessSignal {
16    SingleFile,
17    MultiFile,
18    CrossModule,
19    Unknown,
20}
21
22impl CompletenessSignal {
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            Self::SingleFile => "SCOPE:single-file",
26            Self::MultiFile => "SCOPE:multi-file",
27            Self::CrossModule => "SCOPE:cross-module",
28            Self::Unknown => "SCOPE:unknown",
29        }
30    }
31}
32
33pub fn build_briefing(task: &str, file_context: &[(String, usize)]) -> TaskBriefing {
34    let classification = classify(task);
35
36    let completeness = estimate_completeness(&classification, file_context);
37    let context_hints = build_context_hints(&classification, file_context);
38
39    let output_instruction = classification.task_type.output_format().instruction();
40    let lab_thinking_instruction = classification.task_type.thinking_budget().instruction();
41
42    TaskBriefing {
43        classification,
44        completeness_signal: completeness,
45        output_instruction,
46        context_hints,
47        lab_thinking_instruction,
48    }
49}
50
51fn estimate_completeness(
52    classification: &TaskClassification,
53    file_context: &[(String, usize)],
54) -> CompletenessSignal {
55    if file_context.is_empty() {
56        return CompletenessSignal::Unknown;
57    }
58
59    let unique_dirs: std::collections::HashSet<&str> = file_context
60        .iter()
61        .filter_map(|(path, _)| std::path::Path::new(path).parent().and_then(|p| p.to_str()))
62        .collect();
63
64    if classification.targets.len() <= 1 && unique_dirs.len() <= 1 {
65        CompletenessSignal::SingleFile
66    } else if unique_dirs.len() <= 3 {
67        CompletenessSignal::MultiFile
68    } else {
69        CompletenessSignal::CrossModule
70    }
71}
72
73fn build_context_hints(
74    classification: &TaskClassification,
75    file_context: &[(String, usize)],
76) -> Vec<String> {
77    let mut hints = Vec::new();
78
79    match classification.task_type {
80        TaskType::Generate => {
81            hints.push("Pattern: match existing code style in context".to_string());
82            if !classification.targets.is_empty() {
83                hints.push(format!(
84                    "Insert near: {}",
85                    classification.targets.join(", ")
86                ));
87            }
88        }
89        TaskType::FixBug => {
90            hints.push("Focus: identify root cause, minimal fix".to_string());
91            if let Some(largest) = file_context.iter().max_by_key(|(_, lines)| *lines) {
92                hints.push(format!("Primary file: {} ({}L)", largest.0, largest.1));
93            }
94        }
95        TaskType::Refactor => {
96            hints.push("Preserve: all public APIs and behavior".to_string());
97            hints.push(format!("Files in scope: {}", file_context.len()));
98        }
99        TaskType::Explore => {
100            hints.push("Depth: signatures + key logic, skip boilerplate".to_string());
101        }
102        TaskType::Test => {
103            hints.push("Pattern: follow existing test patterns in codebase".to_string());
104        }
105        TaskType::Debug => {
106            hints.push("Trace: follow data flow through call chain".to_string());
107        }
108        _ => {}
109    }
110
111    hints
112}
113
114pub fn format_briefing(briefing: &TaskBriefing) -> String {
115    let mut parts = Vec::new();
116
117    parts.push(format!(
118        "[TASK:{} {}]",
119        briefing.classification.task_type.as_str(),
120        briefing.completeness_signal.as_str(),
121    ));
122
123    parts.push(briefing.output_instruction.to_string());
124
125    if !briefing.context_hints.is_empty() {
126        for hint in &briefing.context_hints {
127            parts.push(format!("• {hint}"));
128        }
129    }
130
131    parts.join("\n")
132}
133
134pub fn inject_into_instructions(base_instructions: &str, task: &str) -> String {
135    if task.trim().is_empty() {
136        return base_instructions.to_string();
137    }
138
139    let file_context: Vec<(String, usize)> = Vec::new();
140    let briefing = build_briefing(task, &file_context);
141    let briefing_block = format_briefing(&briefing);
142
143    format!("{base_instructions}\n\n{briefing_block}")
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn briefing_for_generate_task() {
152        let files = vec![("src/core/entropy.rs".to_string(), 120)];
153        let briefing = build_briefing("add normalized_token_entropy to entropy.rs", &files);
154        assert_eq!(briefing.classification.task_type, TaskType::Generate);
155        assert!(briefing.output_instruction.contains("code blocks"));
156        assert!(briefing.lab_thinking_instruction.contains("Skip analysis"));
157    }
158
159    #[test]
160    fn briefing_for_fix_bug() {
161        let files = vec![
162            ("src/core/entropy.rs".to_string(), 200),
163            ("src/core/tokens.rs".to_string(), 50),
164        ];
165        let briefing = build_briefing("fix the NaN bug in token_entropy", &files);
166        assert_eq!(briefing.classification.task_type, TaskType::FixBug);
167        assert!(briefing.output_instruction.contains("changed lines"));
168        assert!(!briefing.lab_thinking_instruction.is_empty());
169    }
170
171    #[test]
172    fn completeness_single_file() {
173        let files = vec![("src/core/entropy.rs".to_string(), 200)];
174        let briefing = build_briefing("add a function", &files);
175        matches!(briefing.completeness_signal, CompletenessSignal::SingleFile);
176    }
177
178    #[test]
179    fn completeness_cross_module() {
180        let files = vec![
181            ("src/core/a.rs".to_string(), 100),
182            ("src/tools/b.rs".to_string(), 100),
183            ("src/server.rs".to_string(), 100),
184            ("tests/integration.rs".to_string(), 100),
185        ];
186        let briefing = build_briefing("refactor compression pipeline", &files);
187        matches!(
188            briefing.completeness_signal,
189            CompletenessSignal::CrossModule
190        );
191    }
192
193    #[test]
194    fn format_briefing_includes_all_sections() {
195        let files = vec![("src/core/entropy.rs".to_string(), 120)];
196        let briefing = build_briefing("fix bug in entropy.rs", &files);
197        let formatted = format_briefing(&briefing);
198        assert!(formatted.contains("[TASK:"));
199        assert!(formatted.contains("OUTPUT-HINT:"));
200        assert!(formatted.contains("SCOPE:"));
201    }
202
203    #[test]
204    fn inject_empty_task_unchanged() {
205        let base = "some instructions";
206        let result = inject_into_instructions(base, "");
207        assert_eq!(result, base);
208    }
209
210    #[test]
211    fn briefing_covers_all_task_types() {
212        let scenarios: &[(&str, &str)] = &[
213            ("add a new function to entropy.rs", "generate"),
214            ("fix the bug in token_optimizer.rs", "fix_bug"),
215            ("how does the session cache work?", "explore"),
216            ("refactor compression pipeline", "refactor"),
217            ("write unit tests for entropy", "test"),
218            ("debug why compression ratio drops", "debug"),
219        ];
220        for &(task, expected_type) in scenarios {
221            let briefing = build_briefing(task, &[("src/main.rs".to_string(), 100)]);
222            assert_eq!(
223                briefing.classification.task_type.as_str(),
224                expected_type,
225                "Task '{}' should be classified as '{}'",
226                task,
227                expected_type,
228            );
229            let formatted = format_briefing(&briefing);
230            assert!(formatted.contains("[TASK:"));
231            assert!(formatted.contains("OUTPUT-HINT:"));
232        }
233    }
234}