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