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