|
1 | | -//! Analyze command implementation |
| 1 | +//! Analyze command — migration stub. |
2 | 2 | //! |
3 | | -//! Analyzes spec complexity and structure. |
| 3 | +//! Awaiting migration to the adapter API (tracked in the subsequent CLI |
| 4 | +//! migration specs). Replaced as part of the broader adapter rollout. |
4 | 5 |
|
5 | | -use colored::Colorize; |
6 | | -use leanspec_core::{DependencyGraph, SpecLoader, TokenCounter, TokenStatus}; |
7 | 6 | use std::error::Error; |
8 | 7 |
|
9 | | -pub fn run(specs_dir: &str, spec: &str, output_format: &str) -> Result<(), Box<dyn Error>> { |
10 | | - let loader = SpecLoader::new(specs_dir); |
11 | | - let all_specs = loader.load_all()?; |
12 | | - |
13 | | - let spec_info = loader |
14 | | - .load(spec)? |
15 | | - .ok_or_else(|| format!("Spec not found: {}", spec))?; |
16 | | - |
17 | | - // Read full content for analysis |
18 | | - let content = std::fs::read_to_string(&spec_info.file_path)?; |
19 | | - |
20 | | - // Token counting |
21 | | - let token_counter = TokenCounter::new(); |
22 | | - let token_result = token_counter.count_spec(&content); |
23 | | - |
24 | | - // Dependency analysis |
25 | | - let dep_graph = DependencyGraph::new(&all_specs); |
26 | | - let complete_deps = dep_graph.get_complete_graph(&spec_info.path); |
27 | | - |
28 | | - // Structure analysis |
29 | | - let lines: Vec<&str> = content.lines().collect(); |
30 | | - let total_lines = lines.len(); |
31 | | - |
32 | | - // Count sections (h2 headers) |
33 | | - let sections: Vec<(String, usize)> = lines |
34 | | - .iter() |
35 | | - .enumerate() |
36 | | - .filter(|(_, line)| line.starts_with("## ")) |
37 | | - .map(|(i, line)| { |
38 | | - let title = line.trim_start_matches("## ").to_string(); |
39 | | - // Count lines until next section |
40 | | - let next_section = lines |
41 | | - .iter() |
42 | | - .skip(i + 1) |
43 | | - .position(|l| l.starts_with("## ") || l.starts_with("# ")) |
44 | | - .unwrap_or(lines.len() - i - 1); |
45 | | - (title, next_section) |
46 | | - }) |
47 | | - .collect(); |
48 | | - |
49 | | - // Code block analysis |
50 | | - let code_blocks: Vec<(&str, usize)> = { |
51 | | - let mut blocks = Vec::new(); |
52 | | - let mut in_block = false; |
53 | | - let mut block_lang = ""; |
54 | | - let mut block_lines = 0; |
55 | | - |
56 | | - for line in &lines { |
57 | | - if line.starts_with("```") { |
58 | | - if in_block { |
59 | | - blocks.push((block_lang, block_lines)); |
60 | | - in_block = false; |
61 | | - block_lines = 0; |
62 | | - } else { |
63 | | - in_block = true; |
64 | | - block_lang = line |
65 | | - .trim_start_matches("```") |
66 | | - .split_whitespace() |
67 | | - .next() |
68 | | - .unwrap_or(""); |
69 | | - } |
70 | | - } else if in_block { |
71 | | - block_lines += 1; |
72 | | - } |
73 | | - } |
74 | | - blocks |
75 | | - }; |
76 | | - |
77 | | - // Checklist analysis |
78 | | - let checkboxes: (usize, usize) = { |
79 | | - let completed = lines |
80 | | - .iter() |
81 | | - .filter(|l| l.contains("[x]") || l.contains("[X]")) |
82 | | - .count(); |
83 | | - let incomplete = lines.iter().filter(|l| l.contains("[ ]")).count(); |
84 | | - (completed, completed + incomplete) |
85 | | - }; |
86 | | - |
87 | | - // Calculate complexity score (0-100) |
88 | | - let complexity_score = calculate_complexity( |
89 | | - total_lines, |
90 | | - token_result.total, |
91 | | - sections.len(), |
92 | | - complete_deps |
93 | | - .as_ref() |
94 | | - .map(|d| d.depends_on.len()) |
95 | | - .unwrap_or(0), |
96 | | - code_blocks.len(), |
97 | | - ); |
98 | | - |
99 | | - let complexity_label = match complexity_score { |
100 | | - 0..=30 => "Low", |
101 | | - 31..=60 => "Medium", |
102 | | - 61..=80 => "High", |
103 | | - _ => "Very High", |
104 | | - }; |
105 | | - |
106 | | - if output_format == "json" { |
107 | | - #[derive(serde::Serialize)] |
108 | | - struct Output { |
109 | | - spec: String, |
110 | | - title: String, |
111 | | - complexity: ComplexityOutput, |
112 | | - tokens: TokenOutput, |
113 | | - structure: StructureOutput, |
114 | | - dependencies: DependencyOutput, |
115 | | - } |
116 | | - |
117 | | - #[derive(serde::Serialize)] |
118 | | - struct ComplexityOutput { |
119 | | - score: u32, |
120 | | - label: String, |
121 | | - } |
122 | | - |
123 | | - #[derive(serde::Serialize)] |
124 | | - struct TokenOutput { |
125 | | - total: usize, |
126 | | - frontmatter: usize, |
127 | | - content: usize, |
128 | | - status: String, |
129 | | - } |
130 | | - |
131 | | - #[derive(serde::Serialize)] |
132 | | - struct StructureOutput { |
133 | | - total_lines: usize, |
134 | | - sections: Vec<SectionOutput>, |
135 | | - code_blocks: usize, |
136 | | - code_lines: usize, |
137 | | - checkboxes_completed: usize, |
138 | | - checkboxes_total: usize, |
139 | | - } |
140 | | - |
141 | | - #[derive(serde::Serialize)] |
142 | | - struct SectionOutput { |
143 | | - title: String, |
144 | | - lines: usize, |
145 | | - } |
146 | | - |
147 | | - #[derive(serde::Serialize)] |
148 | | - struct DependencyOutput { |
149 | | - depends_on_count: usize, |
150 | | - required_by_count: usize, |
151 | | - has_circular: bool, |
152 | | - } |
153 | | - |
154 | | - let output = Output { |
155 | | - spec: spec_info.path.clone(), |
156 | | - title: spec_info.title.clone(), |
157 | | - complexity: ComplexityOutput { |
158 | | - score: complexity_score, |
159 | | - label: complexity_label.to_string(), |
160 | | - }, |
161 | | - tokens: TokenOutput { |
162 | | - total: token_result.total, |
163 | | - frontmatter: token_result.frontmatter, |
164 | | - content: token_result.content, |
165 | | - status: format!("{:?}", token_result.status), |
166 | | - }, |
167 | | - structure: StructureOutput { |
168 | | - total_lines, |
169 | | - sections: sections |
170 | | - .iter() |
171 | | - .map(|(t, l)| SectionOutput { |
172 | | - title: t.clone(), |
173 | | - lines: *l, |
174 | | - }) |
175 | | - .collect(), |
176 | | - code_blocks: code_blocks.len(), |
177 | | - code_lines: code_blocks.iter().map(|(_, l)| l).sum(), |
178 | | - checkboxes_completed: checkboxes.0, |
179 | | - checkboxes_total: checkboxes.1, |
180 | | - }, |
181 | | - dependencies: DependencyOutput { |
182 | | - depends_on_count: complete_deps |
183 | | - .as_ref() |
184 | | - .map(|d| d.depends_on.len()) |
185 | | - .unwrap_or(0), |
186 | | - required_by_count: complete_deps |
187 | | - .as_ref() |
188 | | - .map(|d| d.required_by.len()) |
189 | | - .unwrap_or(0), |
190 | | - has_circular: dep_graph.has_circular_dependency(&spec_info.path), |
191 | | - }, |
192 | | - }; |
193 | | - |
194 | | - println!("{}", serde_json::to_string_pretty(&output)?); |
195 | | - return Ok(()); |
196 | | - } |
197 | | - |
198 | | - // Pretty print |
199 | | - println!(); |
200 | | - println!("{}", "═".repeat(60).dimmed()); |
201 | | - println!( |
202 | | - "{}", |
203 | | - format!("Spec Analysis: {}", spec_info.path).bold().cyan() |
204 | | - ); |
205 | | - println!("{}", "═".repeat(60).dimmed()); |
206 | | - println!(); |
207 | | - |
208 | | - // Complexity |
209 | | - let complexity_color = match complexity_score { |
210 | | - 0..=30 => "green", |
211 | | - 31..=60 => "yellow", |
212 | | - 61..=80 => "red", |
213 | | - _ => "red", |
214 | | - }; |
215 | | - println!("{}", "Complexity".bold()); |
216 | | - println!( |
217 | | - " Score: {} ({})", |
218 | | - format!("{}/100", complexity_score) |
219 | | - .color(complexity_color) |
220 | | - .bold(), |
221 | | - complexity_label.color(complexity_color) |
222 | | - ); |
223 | | - println!(); |
224 | | - |
225 | | - // Tokens |
226 | | - println!("{}", "Tokens".bold()); |
227 | | - let token_status_color = match token_result.status { |
228 | | - TokenStatus::Optimal => "green", |
229 | | - TokenStatus::Good => "cyan", |
230 | | - TokenStatus::Warning => "yellow", |
231 | | - TokenStatus::Excessive => "red", |
232 | | - }; |
233 | | - println!( |
234 | | - " Total: {} ({:?})", |
235 | | - token_result |
236 | | - .total |
237 | | - .to_string() |
238 | | - .color(token_status_color) |
239 | | - .bold(), |
240 | | - token_result.status |
241 | | - ); |
242 | | - println!(" Frontmatter: {}", token_result.frontmatter); |
243 | | - println!(" Content: {}", token_result.content); |
244 | | - println!(); |
245 | | - |
246 | | - // Structure |
247 | | - println!("{}", "Structure".bold()); |
248 | | - println!(" Lines: {}", total_lines); |
249 | | - println!(" Sections: {}", sections.len()); |
250 | | - for (title, lines) in §ions { |
251 | | - println!(" • {} ({} lines)", title.cyan(), lines); |
252 | | - } |
253 | | - println!( |
254 | | - " Code blocks: {} ({} lines)", |
255 | | - code_blocks.len(), |
256 | | - code_blocks.iter().map(|(_, l)| l).sum::<usize>() |
257 | | - ); |
258 | | - if checkboxes.1 > 0 { |
259 | | - let checkbox_pct = (checkboxes.0 as f64 / checkboxes.1 as f64 * 100.0) as usize; |
260 | | - println!( |
261 | | - " Checkboxes: {}/{} ({}%)", |
262 | | - checkboxes.0, checkboxes.1, checkbox_pct |
263 | | - ); |
264 | | - } |
265 | | - println!(); |
266 | | - |
267 | | - // Dependencies |
268 | | - println!("{}", "Dependencies".bold()); |
269 | | - if let Some(deps) = complete_deps { |
270 | | - println!(" Depends on: {}", deps.depends_on.len()); |
271 | | - for dep in &deps.depends_on { |
272 | | - println!(" → {}", dep.path.dimmed()); |
273 | | - } |
274 | | - println!(" Required by: {}", deps.required_by.len()); |
275 | | - for req in &deps.required_by { |
276 | | - println!(" ← {}", req.path.dimmed()); |
277 | | - } |
278 | | - |
279 | | - if dep_graph.has_circular_dependency(&spec_info.path) { |
280 | | - println!(" {} Circular dependency detected!", "⚠".yellow()); |
281 | | - } |
282 | | - } else { |
283 | | - println!(" No dependencies"); |
284 | | - } |
285 | | - |
286 | | - println!(); |
287 | | - println!("{}", "─".repeat(60).dimmed()); |
288 | | - |
289 | | - // Recommendations |
290 | | - let mut recommendations: Vec<String> = Vec::new(); |
291 | | - |
292 | | - if token_result.total > 3500 { |
293 | | - recommendations.push("Consider splitting spec - token count is high".to_string()); |
294 | | - } |
295 | | - if total_lines > 400 { |
296 | | - recommendations.push("Spec is quite long - consider focusing content".to_string()); |
297 | | - } |
298 | | - if code_blocks.iter().map(|(_, l)| l).sum::<usize>() > 100 { |
299 | | - recommendations.push("Many code lines - move examples to separate files?".to_string()); |
300 | | - } |
301 | | - if complexity_score > 70 { |
302 | | - recommendations.push("High complexity - consider breaking into sub-specs".to_string()); |
303 | | - } |
304 | | - |
305 | | - if !recommendations.is_empty() { |
306 | | - println!("{}", "Recommendations".bold()); |
307 | | - for rec in &recommendations { |
308 | | - println!(" {} {}", "→".yellow(), rec); |
309 | | - } |
310 | | - } else { |
311 | | - println!("{} Spec looks well-structured", "✓".green()); |
312 | | - } |
313 | | - |
314 | | - println!(); |
315 | | - |
316 | | - Ok(()) |
317 | | -} |
318 | | - |
319 | | -fn calculate_complexity( |
320 | | - lines: usize, |
321 | | - tokens: usize, |
322 | | - sections: usize, |
323 | | - dependencies: usize, |
324 | | - code_blocks: usize, |
325 | | -) -> u32 { |
326 | | - let mut score = 0u32; |
327 | | - |
328 | | - // Lines contribution (0-25 points) |
329 | | - score += match lines { |
330 | | - 0..=100 => 0, |
331 | | - 101..=200 => 5, |
332 | | - 201..=300 => 10, |
333 | | - 301..=400 => 15, |
334 | | - 401..=500 => 20, |
335 | | - _ => 25, |
336 | | - }; |
337 | | - |
338 | | - // Tokens contribution (0-30 points) |
339 | | - score += match tokens { |
340 | | - 0..=1500 => 0, |
341 | | - 1501..=2500 => 5, |
342 | | - 2501..=3500 => 10, |
343 | | - 3501..=4500 => 20, |
344 | | - _ => 30, |
345 | | - }; |
346 | | - |
347 | | - // Sections contribution (0-15 points) |
348 | | - score += match sections { |
349 | | - 0..=3 => 0, |
350 | | - 4..=6 => 5, |
351 | | - 7..=10 => 10, |
352 | | - _ => 15, |
353 | | - }; |
354 | | - |
355 | | - // Dependencies contribution (0-15 points) |
356 | | - score += match dependencies { |
357 | | - 0..=2 => 0, |
358 | | - 3..=5 => 5, |
359 | | - 6..=10 => 10, |
360 | | - _ => 15, |
361 | | - }; |
362 | | - |
363 | | - // Code blocks contribution (0-15 points) |
364 | | - score += match code_blocks { |
365 | | - 0..=2 => 0, |
366 | | - 3..=5 => 5, |
367 | | - 6..=10 => 10, |
368 | | - _ => 15, |
369 | | - }; |
370 | | - |
371 | | - score.min(100) |
| 8 | +pub fn run(_specs_dir: &str, _spec: &str, _output_format: &str) -> Result<(), Box<dyn Error>> { |
| 9 | + Err("`analyze` is not yet migrated to the adapter API".into()) |
372 | 10 | } |
0 commit comments