Skip to content

Commit 4b7c83e

Browse files
tikazyqclaude
andauthored
feat(cli): migrate list command to adapter API (#264) (#294)
Reference implementation for the CLI adapter migration. `leanspec list` now reads the project's configured adapter (markdown or GitHub), uses `SpecSchema` semantic hints to translate `--status`, `--priority`, `--tag`, and `--assignee` into a generic `ListFilter::fields` map, and renders rows from `SpecDoc.fields` using the schema's declared enum colors. The `--specs-dir` flag now errors cleanly when the project adapter is non-markdown rather than silently overriding it. Adds `AdapterRegistry::project_config` so callers can inspect the configured adapter name before instantiating it — important for adapters like GitHub that require auth at construction time. Other CLI commands that still reach into `SpecLoader`/`SpecInfo` are left as stubs returning a "not yet migrated" error. They cover the same set of broken commands flagged by ea92e01 as the migration map for downstream specs (390+). The TUI stays in this stubbed bucket. Test plan - New unit tests in `commands/list.rs` cover the semantic-hint translation, the silent-drop behaviour for missing fields, hex parsing, and parent-link extraction. - E2E tests in `tests/list.rs` create markdown specs directly on disk (because `create`/`update` are stubbed) and exercise the markdown filter, JSON output, the `--specs-dir`-on-GitHub error, and the `--priority`-on-empty-project no-op. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 67aa76f commit 4b7c83e

24 files changed

Lines changed: 760 additions & 5126 deletions

File tree

Lines changed: 5 additions & 367 deletions
Original file line numberDiff line numberDiff line change
@@ -1,372 +1,10 @@
1-
//! Analyze command implementation
1+
//! Analyze command — migration stub.
22
//!
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.
45
5-
use colored::Colorize;
6-
use leanspec_core::{DependencyGraph, SpecLoader, TokenCounter, TokenStatus};
76
use std::error::Error;
87

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 &sections {
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())
37210
}

0 commit comments

Comments
 (0)