Coverage for little_loops / cli / history.py: 97%

77 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-04-11 23:20 -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_args import add_config_arg 

9from little_loops.config import BRConfig 

10 

11 

12def main_history() -> int: 

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

14 

15 Display summary statistics and analysis for completed issues. 

16 

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 ) 

32 

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 ) 

48 

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

50 

51 # summary subcommand (existing) 

52 summary_parser = subparsers.add_parser("summary", help="Show issue statistics") 

53 summary_parser.add_argument( 

54 "-j", 

55 "--json", 

56 action="store_true", 

57 help="Output as JSON instead of formatted text", 

58 ) 

59 summary_parser.add_argument( 

60 "-d", 

61 "--directory", 

62 type=Path, 

63 default=None, 

64 help="Path to issues directory (default: .issues)", 

65 ) 

66 

67 # analyze subcommand (new - FEAT-110) 

68 analyze_parser = subparsers.add_parser( 

69 "analyze", 

70 help="Full analysis with trends, subsystems, and debt metrics", 

71 ) 

72 analyze_parser.add_argument( 

73 "-f", 

74 "--format", 

75 type=str, 

76 choices=["text", "json", "markdown", "yaml"], 

77 default="text", 

78 help="Output format (default: text)", 

79 ) 

80 analyze_parser.add_argument( 

81 "-d", 

82 "--directory", 

83 type=Path, 

84 default=None, 

85 help="Path to issues directory (default: .issues)", 

86 ) 

87 analyze_parser.add_argument( 

88 "-p", 

89 "--period", 

90 type=str, 

91 choices=["weekly", "monthly", "quarterly"], 

92 default="monthly", 

93 help="Grouping period for trends (default: monthly)", 

94 ) 

95 date_filter_group = analyze_parser.add_mutually_exclusive_group() 

96 date_filter_group.add_argument( 

97 "-c", 

98 "--compare", 

99 type=int, 

100 default=None, 

101 metavar="DAYS", 

102 help="Compare last N days to previous N days", 

103 ) 

104 date_filter_group.add_argument( 

105 "--since", 

106 "-S", 

107 type=str, 

108 default=None, 

109 metavar="DATE", 

110 help="Only analyze issues completed on or after DATE (YYYY-MM-DD)", 

111 ) 

112 analyze_parser.add_argument( 

113 "--until", 

114 type=str, 

115 default=None, 

116 metavar="DATE", 

117 help="Only analyze issues completed on or before DATE (YYYY-MM-DD)", 

118 ) 

119 

120 # export subcommand (FEAT-503, renamed from generate-docs in ENH-523) 

121 gendocs_parser = subparsers.add_parser( 

122 "export", 

123 help="Export topic-filtered excerpts from completed issue history", 

124 ) 

125 gendocs_parser.add_argument( 

126 "topic", 

127 type=str, 

128 help="Topic, area, or system to generate documentation for", 

129 ) 

130 gendocs_parser.add_argument( 

131 "--output", 

132 "-o", 

133 type=Path, 

134 default=None, 

135 help="Write output to file instead of stdout", 

136 ) 

137 gendocs_parser.add_argument( 

138 "-f", 

139 "--format", 

140 type=str, 

141 choices=["narrative", "structured"], 

142 default="narrative", 

143 help="Output format (default: narrative)", 

144 ) 

145 gendocs_parser.add_argument( 

146 "-d", 

147 "--directory", 

148 type=Path, 

149 default=None, 

150 help="Path to issues directory (default: .issues)", 

151 ) 

152 gendocs_parser.add_argument( 

153 "--since", 

154 "-S", 

155 type=str, 

156 default=None, 

157 metavar="DATE", 

158 help="Only include issues completed after DATE (YYYY-MM-DD)", 

159 ) 

160 gendocs_parser.add_argument( 

161 "--min-relevance", 

162 type=float, 

163 default=0.5, 

164 metavar="FLOAT", 

165 help="Minimum relevance score threshold (default: 0.5)", 

166 ) 

167 gendocs_parser.add_argument( 

168 "--type", 

169 type=str, 

170 choices=["BUG", "FEAT", "ENH"], 

171 default=None, 

172 dest="issue_type", 

173 help="Filter by issue type", 

174 ) 

175 gendocs_parser.add_argument( 

176 "--scoring", 

177 type=str, 

178 choices=["intersection", "bm25", "hybrid"], 

179 default="intersection", 

180 help="Relevance scoring method: intersection (default), bm25, or hybrid", 

181 ) 

182 

183 add_config_arg(parser) 

184 

185 args = parser.parse_args() 

186 

187 if not args.command: 

188 parser.print_help() 

189 return 1 

190 

191 # Determine directories 

192 project_root = args.config or Path.cwd() 

193 config = BRConfig(project_root) 

194 issues_dir = args.directory or config.project_root / config.issues.base_dir 

195 completed_dir = issues_dir / "completed" 

196 

197 if args.command == "summary": 

198 # Existing summary logic 

199 issues = scan_completed_issues(completed_dir) 

200 summary = calculate_summary(issues) 

201 

202 if args.json: 

203 print(format_summary_json(summary)) 

204 else: 

205 print(format_summary_text(summary)) 

206 

207 return 0 

208 

209 if args.command == "analyze": 

210 # New analyze logic (FEAT-110) 

211 from datetime import date as date_type 

212 

213 issues = scan_completed_issues(completed_dir) 

214 

215 since_date = date_type.fromisoformat(args.since) if args.since else None 

216 until_date = date_type.fromisoformat(args.until) if args.until else None 

217 if since_date or until_date: 

218 issues = [ 

219 i 

220 for i in issues 

221 if i.completed_date is not None 

222 and (since_date is None or i.completed_date >= since_date) 

223 and (until_date is None or i.completed_date <= until_date) 

224 ] 

225 

226 analysis = calculate_analysis( 

227 issues, 

228 issues_dir=issues_dir, 

229 period_type=args.period, 

230 compare_days=args.compare, 

231 project_root=project_root, 

232 ) 

233 

234 if args.format == "json": 

235 print(format_analysis_json(analysis)) 

236 elif args.format == "yaml": 

237 print(format_analysis_yaml(analysis)) 

238 elif args.format == "markdown": 

239 print(format_analysis_markdown(analysis)) 

240 else: 

241 print(format_analysis_text(analysis)) 

242 

243 return 0 

244 

245 if args.command == "export": 

246 from datetime import date as date_type 

247 

248 from little_loops.issue_history.analysis import _load_issue_contents 

249 

250 issues = scan_completed_issues(completed_dir) 

251 contents = _load_issue_contents(issues) 

252 

253 since_date = None 

254 if args.since: 

255 since_date = date_type.fromisoformat(args.since) 

256 

257 doc = synthesize_docs( 

258 topic=args.topic, 

259 issues=issues, 

260 contents=contents, 

261 format=args.format, 

262 min_relevance=args.min_relevance, 

263 since=since_date, 

264 issue_type=args.issue_type, 

265 scoring=args.scoring, 

266 ) 

267 

268 if args.output: 

269 args.output.parent.mkdir(parents=True, exist_ok=True) 

270 args.output.write_text(doc, encoding="utf-8") 

271 print(f"Documentation written to {args.output}") 

272 else: 

273 print(doc) 

274 

275 return 0 

276 

277 return 1