Coverage for little_loops / cli / history.py: 99%
80 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
1"""ll-history: Display summary statistics and analysis for completed issues."""
3from __future__ import annotations
5import argparse
6from pathlib import Path
8from little_loops.cli.output import configure_output, use_color_enabled
9from little_loops.cli_args import add_config_arg
10from little_loops.config import BRConfig
11from little_loops.logger import Logger
14def main_history() -> int:
15 """Entry point for ll-history command.
17 Display summary statistics and analysis for completed issues.
19 Returns:
20 Exit code (0 = success)
21 """
22 from little_loops.issue_history import (
23 calculate_analysis,
24 calculate_summary,
25 format_analysis_json,
26 format_analysis_markdown,
27 format_analysis_text,
28 format_analysis_yaml,
29 format_summary_json,
30 format_summary_text,
31 scan_completed_issues,
32 synthesize_docs,
33 )
35 parser = argparse.ArgumentParser(
36 prog="ll-history",
37 description="Display summary statistics and analysis for completed issues",
38 formatter_class=argparse.RawDescriptionHelpFormatter,
39 epilog="""
40Examples:
41 %(prog)s summary # Show summary statistics
42 %(prog)s summary --json # Output as JSON
43 %(prog)s analyze # Full analysis report
44 %(prog)s analyze --format markdown # Markdown report
45 %(prog)s analyze --compare 30 # Compare last 30 days to previous
46 %(prog)s export "session log" # Export topic-filtered issue excerpts
47 %(prog)s export "sprint CLI" --output docs/arch/sprint.md
48""",
49 )
51 subparsers = parser.add_subparsers(dest="command", help="Available commands")
53 # summary subcommand (existing)
54 summary_parser = subparsers.add_parser("summary", help="Show issue statistics")
55 summary_parser.add_argument(
56 "-j",
57 "--json",
58 action="store_true",
59 help="Output as JSON instead of formatted text",
60 )
61 summary_parser.add_argument(
62 "-d",
63 "--directory",
64 type=Path,
65 default=None,
66 help="Path to issues directory (default: .issues)",
67 )
69 # analyze subcommand (new - FEAT-110)
70 analyze_parser = subparsers.add_parser(
71 "analyze",
72 help="Full analysis with trends, subsystems, and debt metrics",
73 )
74 analyze_parser.add_argument(
75 "-f",
76 "--format",
77 type=str,
78 choices=["text", "json", "markdown", "yaml"],
79 default="text",
80 help="Output format (default: text)",
81 )
82 analyze_parser.add_argument(
83 "-d",
84 "--directory",
85 type=Path,
86 default=None,
87 help="Path to issues directory (default: .issues)",
88 )
89 analyze_parser.add_argument(
90 "-p",
91 "--period",
92 type=str,
93 choices=["weekly", "monthly", "quarterly"],
94 default="monthly",
95 help="Grouping period for trends (default: monthly)",
96 )
97 date_filter_group = analyze_parser.add_mutually_exclusive_group()
98 date_filter_group.add_argument(
99 "-c",
100 "--compare",
101 type=int,
102 default=None,
103 metavar="DAYS",
104 help="Compare last N days to previous N days",
105 )
106 date_filter_group.add_argument(
107 "--since",
108 "-S",
109 type=str,
110 default=None,
111 metavar="DATE",
112 help="Only analyze issues completed on or after DATE (YYYY-MM-DD)",
113 )
114 analyze_parser.add_argument(
115 "--until",
116 type=str,
117 default=None,
118 metavar="DATE",
119 help="Only analyze issues completed on or before DATE (YYYY-MM-DD)",
120 )
122 # export subcommand (FEAT-503, renamed from generate-docs in ENH-523)
123 gendocs_parser = subparsers.add_parser(
124 "export",
125 help="Export topic-filtered excerpts from completed issue history",
126 )
127 gendocs_parser.add_argument(
128 "topic",
129 type=str,
130 help="Topic, area, or system to generate documentation for",
131 )
132 gendocs_parser.add_argument(
133 "--output",
134 "-o",
135 type=Path,
136 default=None,
137 help="Write output to file instead of stdout",
138 )
139 gendocs_parser.add_argument(
140 "-f",
141 "--format",
142 type=str,
143 choices=["narrative", "structured"],
144 default="narrative",
145 help="Output format (default: narrative)",
146 )
147 gendocs_parser.add_argument(
148 "-d",
149 "--directory",
150 type=Path,
151 default=None,
152 help="Path to issues directory (default: .issues)",
153 )
154 gendocs_parser.add_argument(
155 "--since",
156 "-S",
157 type=str,
158 default=None,
159 metavar="DATE",
160 help="Only include issues completed after DATE (YYYY-MM-DD)",
161 )
162 gendocs_parser.add_argument(
163 "--min-relevance",
164 type=float,
165 default=0.5,
166 metavar="FLOAT",
167 help="Minimum relevance score threshold (default: 0.5)",
168 )
169 gendocs_parser.add_argument(
170 "--type",
171 type=str,
172 choices=["BUG", "FEAT", "ENH", "EPIC"],
173 default=None,
174 dest="issue_type",
175 help="Filter by issue type",
176 )
177 gendocs_parser.add_argument(
178 "--scoring",
179 type=str,
180 choices=["intersection", "bm25", "hybrid"],
181 default="intersection",
182 help="Relevance scoring method: intersection (default), bm25, or hybrid",
183 )
185 add_config_arg(parser)
187 args = parser.parse_args()
189 if not args.command:
190 parser.print_help()
191 return 1
193 # Determine directories
194 project_root = args.config or Path.cwd()
195 config = BRConfig(project_root)
196 configure_output(config.cli)
197 logger = Logger(use_color=use_color_enabled())
198 issues_dir = args.directory or config.project_root / config.issues.base_dir
200 if args.command == "summary":
201 # Existing summary logic
202 issues = scan_completed_issues(issues_dir)
203 summary = calculate_summary(issues)
205 if args.json:
206 print(format_summary_json(summary))
207 else:
208 print(format_summary_text(summary))
210 return 0
212 if args.command == "analyze":
213 # New analyze logic (FEAT-110)
214 from datetime import date as date_type
216 issues = scan_completed_issues(issues_dir)
218 since_date = date_type.fromisoformat(args.since) if args.since else None
219 until_date = date_type.fromisoformat(args.until) if args.until else None
220 if since_date or until_date:
221 issues = [
222 i
223 for i in issues
224 if i.completed_date is not None
225 and (since_date is None or i.completed_date >= since_date)
226 and (until_date is None or i.completed_date <= until_date)
227 ]
229 analysis = calculate_analysis(
230 issues,
231 issues_dir=issues_dir,
232 period_type=args.period,
233 compare_days=args.compare,
234 project_root=project_root,
235 )
237 if args.format == "json":
238 print(format_analysis_json(analysis))
239 elif args.format == "yaml":
240 print(format_analysis_yaml(analysis))
241 elif args.format == "markdown":
242 print(format_analysis_markdown(analysis))
243 else:
244 print(format_analysis_text(analysis))
246 return 0
248 if args.command == "export":
249 from datetime import date as date_type
251 from little_loops.issue_history.analysis import _load_issue_contents
253 issues = scan_completed_issues(issues_dir)
254 contents = _load_issue_contents(issues)
256 since_date = None
257 if args.since:
258 since_date = date_type.fromisoformat(args.since)
260 doc = synthesize_docs(
261 topic=args.topic,
262 issues=issues,
263 contents=contents,
264 format=args.format,
265 min_relevance=args.min_relevance,
266 since=since_date,
267 issue_type=args.issue_type,
268 scoring=args.scoring,
269 )
271 if args.output:
272 args.output.parent.mkdir(parents=True, exist_ok=True)
273 args.output.write_text(doc, encoding="utf-8")
274 logger.success(f"Documentation written to {args.output}")
275 else:
276 print(doc)
278 return 0
280 return 1