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

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

65 

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 ) 

102 

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 ) 

163 

164 add_config_arg(parser) 

165 

166 args = parser.parse_args() 

167 

168 if not args.command: 

169 parser.print_help() 

170 return 1 

171 

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" 

177 

178 if args.command == "summary": 

179 # Existing summary logic 

180 issues = scan_completed_issues(completed_dir) 

181 summary = calculate_summary(issues) 

182 

183 if args.json: 

184 print(format_summary_json(summary)) 

185 else: 

186 print(format_summary_text(summary)) 

187 

188 return 0 

189 

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 ) 

199 

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

208 

209 return 0 

210 

211 if args.command == "export": 

212 from datetime import date as date_type 

213 

214 from little_loops.issue_history.analysis import _load_issue_contents 

215 

216 issues = scan_completed_issues(completed_dir) 

217 contents = _load_issue_contents(issues) 

218 

219 since_date = None 

220 if args.since: 

221 since_date = date_type.fromisoformat(args.since) 

222 

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 ) 

233 

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) 

240 

241 return 0 

242 

243 return 1