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

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 scan_completed_issues_from_db, 

33 synthesize_docs, 

34 ) 

35 from little_loops.session_store import DEFAULT_DB_PATH 

36 

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 ) 

52 

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

54 

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 ) 

70 

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 ) 

123 

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 ) 

186 

187 add_config_arg(parser) 

188 

189 args = parser.parse_args() 

190 

191 if not args.command: 

192 parser.print_help() 

193 return 1 

194 

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 

201 

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) 

210 

211 if args.json: 

212 print(format_summary_json(summary)) 

213 else: 

214 print(format_summary_text(summary)) 

215 

216 return 0 

217 

218 if args.command == "analyze": 

219 # New analyze logic (FEAT-110) 

220 from datetime import date as date_type 

221 

222 issues = scan_completed_issues(issues_dir) 

223 

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 ] 

234 

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 ) 

242 

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

251 

252 return 0 

253 

254 if args.command == "export": 

255 from datetime import date as date_type 

256 

257 from little_loops.issue_history.analysis import _load_issue_contents 

258 

259 issues = scan_completed_issues(issues_dir) 

260 contents = _load_issue_contents(issues) 

261 

262 since_date = None 

263 if args.since: 

264 since_date = date_type.fromisoformat(args.since) 

265 

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 ) 

276 

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) 

283 

284 return 0 

285 

286 return 1