Coverage for little_loops / cli / history.py: 10%
104 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-04 12:21 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-04 12:21 -0500
1"""ll-history: Display summary statistics and analysis for completed issues."""
3from __future__ import annotations
5import argparse
6import sys
7from pathlib import Path
9from little_loops.cli.output import configure_output, print_json, use_color_enabled
10from little_loops.cli_args import add_config_arg, add_json_arg
11from little_loops.config import BRConfig
12from little_loops.logger import Logger
13from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context
16def main_history() -> int:
17 """Entry point for ll-history command.
19 Display summary statistics and analysis for completed issues.
21 Returns:
22 Exit code (0 = success)
23 """
24 with cli_event_context(DEFAULT_DB_PATH, "ll-history", sys.argv[1:]):
25 from little_loops.issue_history import (
26 calculate_analysis,
27 calculate_summary,
28 format_analysis_json,
29 format_analysis_markdown,
30 format_analysis_text,
31 format_analysis_yaml,
32 format_summary_json,
33 format_summary_text,
34 scan_completed_issues,
35 scan_completed_issues_from_db,
36 synthesize_docs,
37 )
39 parser = argparse.ArgumentParser(
40 prog="ll-history",
41 description="Display summary statistics and analysis for completed issues",
42 formatter_class=argparse.RawDescriptionHelpFormatter,
43 epilog="""
44Examples:
45 %(prog)s summary # Show summary statistics
46 %(prog)s summary --json # Output as JSON
47 %(prog)s analyze # Full analysis report
48 %(prog)s analyze --format markdown # Markdown report
49 %(prog)s analyze --compare 30 # Compare last 30 days to previous
50 %(prog)s export "session log" # Export topic-filtered issue excerpts
51 %(prog)s export "sprint CLI" --output docs/arch/sprint.md
52""",
53 )
55 subparsers = parser.add_subparsers(dest="command", help="Available commands")
57 # summary subcommand (existing)
58 summary_parser = subparsers.add_parser("summary", help="Show issue statistics")
59 summary_parser.add_argument(
60 "-j",
61 "--json",
62 action="store_true",
63 help="Output as JSON instead of formatted text",
64 )
65 summary_parser.add_argument(
66 "-d",
67 "--directory",
68 type=Path,
69 default=None,
70 help="Path to issues directory (default: .issues)",
71 )
73 # analyze subcommand (new - FEAT-110)
74 analyze_parser = subparsers.add_parser(
75 "analyze",
76 help="Full analysis with trends, subsystems, and debt metrics",
77 )
78 analyze_parser.add_argument(
79 "-f",
80 "--format",
81 type=str,
82 choices=["text", "json", "markdown", "yaml"],
83 default="text",
84 help="Output format (default: text)",
85 )
86 analyze_parser.add_argument(
87 "-d",
88 "--directory",
89 type=Path,
90 default=None,
91 help="Path to issues directory (default: .issues)",
92 )
93 analyze_parser.add_argument(
94 "-p",
95 "--period",
96 type=str,
97 choices=["weekly", "monthly", "quarterly"],
98 default="monthly",
99 help="Grouping period for trends (default: monthly)",
100 )
101 date_filter_group = analyze_parser.add_mutually_exclusive_group()
102 date_filter_group.add_argument(
103 "-c",
104 "--compare",
105 type=int,
106 default=None,
107 metavar="DAYS",
108 help="Compare last N days to previous N days",
109 )
110 date_filter_group.add_argument(
111 "--since",
112 "-S",
113 type=str,
114 default=None,
115 metavar="DATE",
116 help="Only analyze issues completed on or after DATE (YYYY-MM-DD)",
117 )
118 analyze_parser.add_argument(
119 "--until",
120 type=str,
121 default=None,
122 metavar="DATE",
123 help="Only analyze issues completed on or before DATE (YYYY-MM-DD)",
124 )
126 # export subcommand (FEAT-503, renamed from generate-docs in ENH-523)
127 gendocs_parser = subparsers.add_parser(
128 "export",
129 help="Export topic-filtered excerpts from completed issue history",
130 )
131 gendocs_parser.add_argument(
132 "topic",
133 type=str,
134 help="Topic, area, or system to generate documentation for",
135 )
136 gendocs_parser.add_argument(
137 "--output",
138 "-o",
139 type=Path,
140 default=None,
141 help="Write output to file instead of stdout",
142 )
143 gendocs_parser.add_argument(
144 "-f",
145 "--format",
146 type=str,
147 choices=["narrative", "structured"],
148 default="narrative",
149 help="Output format (default: narrative)",
150 )
151 gendocs_parser.add_argument(
152 "-d",
153 "--directory",
154 type=Path,
155 default=None,
156 help="Path to issues directory (default: .issues)",
157 )
158 gendocs_parser.add_argument(
159 "--since",
160 "-S",
161 type=str,
162 default=None,
163 metavar="DATE",
164 help="Only include issues completed after DATE (YYYY-MM-DD)",
165 )
166 gendocs_parser.add_argument(
167 "--min-relevance",
168 type=float,
169 default=0.5,
170 metavar="FLOAT",
171 help="Minimum relevance score threshold (default: 0.5)",
172 )
173 gendocs_parser.add_argument(
174 "--type",
175 type=str,
176 choices=["BUG", "FEAT", "ENH", "EPIC"],
177 default=None,
178 dest="issue_type",
179 help="Filter by issue type",
180 )
181 gendocs_parser.add_argument(
182 "--scoring",
183 type=str,
184 choices=["intersection", "bm25", "hybrid"],
185 default="intersection",
186 help="Relevance scoring method: intersection (default), bm25, or hybrid",
187 )
189 # sessions subcommand (ENH-1711)
190 sessions_parser = subparsers.add_parser(
191 "sessions",
192 help="List sessions that touched an issue",
193 )
194 sessions_parser.add_argument(
195 "issue_id",
196 metavar="ISSUE_ID",
197 help="Issue ID (e.g., ENH-1710)",
198 )
199 sessions_parser.add_argument(
200 "--limit", type=int, default=20, metavar="N", help="Maximum results (default: 20)"
201 )
202 add_json_arg(sessions_parser)
204 add_config_arg(parser)
206 args = parser.parse_args()
208 if not args.command:
209 parser.print_help()
210 return 1
212 # Determine directories
213 project_root = args.config or Path.cwd()
214 config = BRConfig(project_root)
215 configure_output(config.cli)
216 logger = Logger(use_color=use_color_enabled())
217 issues_dir = (
218 getattr(args, "directory", None) or config.project_root / config.issues.base_dir
219 )
221 if args.command == "summary":
222 # Prefer the unified session DB when populated; fall back to the
223 # file-parsing path when the DB is missing or empty (ENH-1621).
224 db_path = project_root / DEFAULT_DB_PATH
225 issues = scan_completed_issues_from_db(db_path)
226 if not issues:
227 issues = scan_completed_issues(issues_dir)
228 summary = calculate_summary(issues)
230 if args.json:
231 print(format_summary_json(summary))
232 else:
233 print(format_summary_text(summary))
235 return 0
237 if args.command == "analyze":
238 # New analyze logic (FEAT-110)
239 from datetime import date as date_type
241 issues = scan_completed_issues(issues_dir)
243 since_date = date_type.fromisoformat(args.since) if args.since else None
244 until_date = date_type.fromisoformat(args.until) if args.until else None
245 if since_date or until_date:
246 issues = [
247 i
248 for i in issues
249 if i.completed_date is not None
250 and (since_date is None or i.completed_date >= since_date)
251 and (until_date is None or i.completed_date <= until_date)
252 ]
254 analysis = calculate_analysis(
255 issues,
256 issues_dir=issues_dir,
257 period_type=args.period,
258 compare_days=args.compare,
259 project_root=project_root,
260 )
262 if args.format == "json":
263 print(format_analysis_json(analysis))
264 elif args.format == "yaml":
265 print(format_analysis_yaml(analysis))
266 elif args.format == "markdown":
267 print(format_analysis_markdown(analysis))
268 else:
269 print(format_analysis_text(analysis))
271 return 0
273 if args.command == "sessions":
274 from little_loops.history_reader import sessions_for_issue
275 from little_loops.session_store import DEFAULT_DB_PATH as _SS_DB
277 db_path = project_root / _SS_DB
278 refs = sessions_for_issue(args.issue_id, limit=args.limit, db=db_path)
279 if args.json:
280 from dataclasses import asdict
282 print_json([asdict(r) for r in refs])
283 elif not refs:
284 print(f"No sessions found for {args.issue_id}.")
285 else:
286 for r in refs:
287 path = r.jsonl_path or "(no path)"
288 print(f"{r.session_id} {path}")
289 return 0
291 if args.command == "export":
292 from datetime import date as date_type
294 from little_loops.issue_history.analysis import _load_issue_contents
296 issues = scan_completed_issues(issues_dir)
297 contents = _load_issue_contents(issues)
299 since_date = None
300 if args.since:
301 since_date = date_type.fromisoformat(args.since)
303 doc = synthesize_docs(
304 topic=args.topic,
305 issues=issues,
306 contents=contents,
307 format=args.format,
308 min_relevance=args.min_relevance,
309 since=since_date,
310 issue_type=args.issue_type,
311 scoring=args.scoring,
312 )
314 if args.output:
315 args.output.parent.mkdir(parents=True, exist_ok=True)
316 args.output.write_text(doc, encoding="utf-8")
317 logger.success(f"Documentation written to {args.output}")
318 else:
319 print(doc)
321 return 0
323 return 1