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