Coverage for little_loops / cli / history.py: 9%
69 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -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 "--json",
55 action="store_true",
56 help="Output as JSON instead of formatted text",
57 )
58 summary_parser.add_argument(
59 "-d",
60 "--directory",
61 type=Path,
62 default=None,
63 help="Path to issues directory (default: .issues)",
64 )
66 # analyze subcommand (new - FEAT-110)
67 analyze_parser = subparsers.add_parser(
68 "analyze",
69 help="Full analysis with trends, subsystems, and debt metrics",
70 )
71 analyze_parser.add_argument(
72 "-f",
73 "--format",
74 type=str,
75 choices=["text", "json", "markdown", "yaml"],
76 default="text",
77 help="Output format (default: text)",
78 )
79 analyze_parser.add_argument(
80 "-d",
81 "--directory",
82 type=Path,
83 default=None,
84 help="Path to issues directory (default: .issues)",
85 )
86 analyze_parser.add_argument(
87 "-p",
88 "--period",
89 type=str,
90 choices=["weekly", "monthly", "quarterly"],
91 default="monthly",
92 help="Grouping period for trends (default: monthly)",
93 )
94 analyze_parser.add_argument(
95 "-c",
96 "--compare",
97 type=int,
98 default=None,
99 metavar="DAYS",
100 help="Compare last N days to previous N days",
101 )
103 # export subcommand (FEAT-503, renamed from generate-docs in ENH-523)
104 gendocs_parser = subparsers.add_parser(
105 "export",
106 help="Export topic-filtered excerpts from completed issue history",
107 )
108 gendocs_parser.add_argument(
109 "topic",
110 type=str,
111 help="Topic, area, or system to generate documentation for",
112 )
113 gendocs_parser.add_argument(
114 "--output",
115 type=Path,
116 default=None,
117 help="Write output to file instead of stdout",
118 )
119 gendocs_parser.add_argument(
120 "-f",
121 "--format",
122 type=str,
123 choices=["narrative", "structured"],
124 default="narrative",
125 help="Output format (default: narrative)",
126 )
127 gendocs_parser.add_argument(
128 "-d",
129 "--directory",
130 type=Path,
131 default=None,
132 help="Path to issues directory (default: .issues)",
133 )
134 gendocs_parser.add_argument(
135 "--since",
136 type=str,
137 default=None,
138 metavar="DATE",
139 help="Only include issues completed after DATE (YYYY-MM-DD)",
140 )
141 gendocs_parser.add_argument(
142 "--min-relevance",
143 type=float,
144 default=0.5,
145 metavar="FLOAT",
146 help="Minimum relevance score threshold (default: 0.5)",
147 )
148 gendocs_parser.add_argument(
149 "--type",
150 type=str,
151 choices=["BUG", "FEAT", "ENH"],
152 default=None,
153 dest="issue_type",
154 help="Filter by issue type",
155 )
156 gendocs_parser.add_argument(
157 "--scoring",
158 type=str,
159 choices=["intersection", "bm25", "hybrid"],
160 default="intersection",
161 help="Relevance scoring method: intersection (default), bm25, or hybrid",
162 )
164 add_config_arg(parser)
166 args = parser.parse_args()
168 if not args.command:
169 parser.print_help()
170 return 1
172 # Determine directories
173 project_root = args.config or Path.cwd()
174 config = BRConfig(project_root)
175 issues_dir = args.directory or config.project_root / config.issues.base_dir
176 completed_dir = issues_dir / "completed"
178 if args.command == "summary":
179 # Existing summary logic
180 issues = scan_completed_issues(completed_dir)
181 summary = calculate_summary(issues)
183 if args.json:
184 print(format_summary_json(summary))
185 else:
186 print(format_summary_text(summary))
188 return 0
190 if args.command == "analyze":
191 # New analyze logic (FEAT-110)
192 issues = scan_completed_issues(completed_dir)
193 analysis = calculate_analysis(
194 issues,
195 issues_dir=issues_dir,
196 period_type=args.period,
197 compare_days=args.compare,
198 )
200 if args.format == "json":
201 print(format_analysis_json(analysis))
202 elif args.format == "yaml":
203 print(format_analysis_yaml(analysis))
204 elif args.format == "markdown":
205 print(format_analysis_markdown(analysis))
206 else:
207 print(format_analysis_text(analysis))
209 return 0
211 if args.command == "export":
212 from datetime import date as date_type
214 from little_loops.issue_history.analysis import _load_issue_contents
216 issues = scan_completed_issues(completed_dir)
217 contents = _load_issue_contents(issues)
219 since_date = None
220 if args.since:
221 since_date = date_type.fromisoformat(args.since)
223 doc = synthesize_docs(
224 topic=args.topic,
225 issues=issues,
226 contents=contents,
227 format=args.format,
228 min_relevance=args.min_relevance,
229 since=since_date,
230 issue_type=args.issue_type,
231 scoring=args.scoring,
232 )
234 if args.output:
235 args.output.parent.mkdir(parents=True, exist_ok=True)
236 args.output.write_text(doc, encoding="utf-8")
237 print(f"Documentation written to {args.output}")
238 else:
239 print(doc)
241 return 0
243 return 1