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

1"""ll-history: Display summary statistics and analysis for completed issues.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6from pathlib import Path 

7 

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 

12 

13 

14def main_history() -> int: 

15 """Entry point for ll-history command. 

16 

17 Display summary statistics and analysis for completed issues. 

18 

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 ) 

34 

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 ) 

50 

51 subparsers = parser.add_subparsers(dest="command", help="Available commands") 

52 

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 ) 

68 

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 ) 

121 

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 ) 

184 

185 add_config_arg(parser) 

186 

187 args = parser.parse_args() 

188 

189 if not args.command: 

190 parser.print_help() 

191 return 1 

192 

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 

199 

200 if args.command == "summary": 

201 # Existing summary logic 

202 issues = scan_completed_issues(issues_dir) 

203 summary = calculate_summary(issues) 

204 

205 if args.json: 

206 print(format_summary_json(summary)) 

207 else: 

208 print(format_summary_text(summary)) 

209 

210 return 0 

211 

212 if args.command == "analyze": 

213 # New analyze logic (FEAT-110) 

214 from datetime import date as date_type 

215 

216 issues = scan_completed_issues(issues_dir) 

217 

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 ] 

228 

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 ) 

236 

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)) 

245 

246 return 0 

247 

248 if args.command == "export": 

249 from datetime import date as date_type 

250 

251 from little_loops.issue_history.analysis import _load_issue_contents 

252 

253 issues = scan_completed_issues(issues_dir) 

254 contents = _load_issue_contents(issues) 

255 

256 since_date = None 

257 if args.since: 

258 since_date = date_type.fromisoformat(args.since) 

259 

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 ) 

270 

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) 

277 

278 return 0 

279 

280 return 1