1use crate::core::intent_engine::{TaskClassification, TaskType, classify};
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 format_briefing_with(briefing, true)
116}
117
118pub fn format_briefing_with(briefing: &TaskBriefing, include_output_hint: bool) -> String {
126 let mut parts = Vec::new();
127
128 parts.push(format!(
129 "[TASK:{} {}]",
130 briefing.classification.task_type.as_str(),
131 briefing.completeness_signal.as_str(),
132 ));
133
134 if include_output_hint {
135 parts.push(briefing.output_instruction.to_string());
136 }
137
138 if !briefing.context_hints.is_empty() {
139 for hint in &briefing.context_hints {
140 parts.push(format!("• {hint}"));
141 }
142 }
143
144 parts.join("\n")
145}
146
147pub fn inject_into_instructions(base_instructions: &str, task: &str) -> String {
148 if task.trim().is_empty() {
149 return base_instructions.to_string();
150 }
151
152 let file_context: Vec<(String, usize)> = Vec::new();
153 let briefing = build_briefing(task, &file_context);
154 let briefing_block = format_briefing(&briefing);
155
156 format!("{base_instructions}\n\n{briefing_block}")
157}
158
159#[cfg(test)]
160pub mod tests {
161 use super::*;
162
163 #[test]
164 fn briefing_for_generate_task() {
165 let files = vec![("src/core/entropy.rs".to_string(), 120)];
166 let briefing = build_briefing("add normalized_token_entropy to entropy.rs", &files);
167 assert_eq!(briefing.classification.task_type, TaskType::Generate);
168 assert!(briefing.output_instruction.contains("code blocks"));
169 assert!(briefing.lab_thinking_instruction.contains("Skip analysis"));
170 }
171
172 #[test]
173 fn briefing_for_fix_bug() {
174 let files = vec![
175 ("src/core/entropy.rs".to_string(), 200),
176 ("src/core/tokens.rs".to_string(), 50),
177 ];
178 let briefing = build_briefing("fix the NaN bug in token_entropy", &files);
179 assert_eq!(briefing.classification.task_type, TaskType::FixBug);
180 assert!(briefing.output_instruction.contains("changed lines"));
181 assert!(
182 !briefing.lab_thinking_instruction.is_empty(),
183 "`briefing.lab_thinking_instruction` is empty"
184 );
185 }
186
187 #[test]
188 fn completeness_single_file() {
189 let files = vec![("src/core/entropy.rs".to_string(), 200)];
190 let briefing = build_briefing("add a function", &files);
191 matches!(briefing.completeness_signal, CompletenessSignal::SingleFile);
192 }
193
194 #[test]
195 fn completeness_cross_module() {
196 let files = vec![
197 ("src/core/a.rs".to_string(), 100),
198 ("src/tools/b.rs".to_string(), 100),
199 ("src/server.rs".to_string(), 100),
200 ("tests/integration.rs".to_string(), 100),
201 ];
202 let briefing = build_briefing("refactor compression pipeline", &files);
203 matches!(
204 briefing.completeness_signal,
205 CompletenessSignal::CrossModule
206 );
207 }
208
209 #[test]
210 fn format_briefing_includes_all_sections() {
211 let files = vec![("src/core/entropy.rs".to_string(), 120)];
212 let briefing = build_briefing("fix bug in entropy.rs", &files);
213 let formatted = format_briefing(&briefing);
214 assert!(formatted.contains("[TASK:"));
215 assert!(formatted.contains("OUTPUT-HINT:"));
216 assert!(formatted.contains("SCOPE:"));
217 }
218
219 #[test]
220 fn format_briefing_without_output_hint_keeps_classification_and_hints() {
221 let files = vec![("src/core/entropy.rs".to_string(), 120)];
224 let briefing = build_briefing("how does the entropy scorer work?", &files);
225 let quiet = format_briefing_with(&briefing, false);
226 assert!(quiet.contains("[TASK:"));
227 assert!(quiet.contains("SCOPE:"));
228 assert!(!quiet.contains("OUTPUT-HINT:"), "{quiet}");
229 assert!(
230 !briefing.context_hints.is_empty(),
231 "explore tasks carry hints"
232 );
233 for hint in &briefing.context_hints {
234 assert!(quiet.contains(hint), "hint '{hint}' must survive");
235 }
236 assert_eq!(
238 format_briefing(&briefing),
239 format_briefing_with(&briefing, true)
240 );
241 }
242
243 #[test]
244 fn inject_empty_task_unchanged() {
245 let base = "some instructions";
246 let result = inject_into_instructions(base, "");
247 assert_eq!(result, base);
248 }
249
250 #[test]
251 fn briefing_covers_all_task_types() {
252 let scenarios: &[(&str, &str)] = &[
253 ("add a new function to entropy.rs", "generate"),
254 ("fix the bug in token_optimizer.rs", "fix_bug"),
255 ("how does the session cache work?", "explore"),
256 ("refactor compression pipeline", "refactor"),
257 ("write unit tests for entropy", "test"),
258 ("debug why compression ratio drops", "debug"),
259 ];
260 for &(task, expected_type) in scenarios {
261 let briefing = build_briefing(task, &[("src/main.rs".to_string(), 100)]);
262 assert_eq!(
263 briefing.classification.task_type.as_str(),
264 expected_type,
265 "Task '{task}' should be classified as '{expected_type}'",
266 );
267 let formatted = format_briefing(&briefing);
268 assert!(formatted.contains("[TASK:"));
269 assert!(formatted.contains("OUTPUT-HINT:"));
270 }
271 }
272}