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

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

2

3 from __future__ import annotations

4

5 import argparse

6 from pathlib import Path

7

8 from little_loops.cli_args import add_config_arg

9 from little_loops.config import BRConfig

10

11

12 def 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="""

38 Examples:

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