Coverage for little_loops / cli / docs.py: 88%

89 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-05-22 16:19 -0500

1"""ll-verify-docs and ll-check-links: Documentation verification commands.""" 

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.logger import Logger 

10 

11 

12def main_verify_docs() -> int: 

13 """Entry point for ll-verify-docs command. 

14 

15 Verify that documented counts (commands, agents, skills) match actual file counts. 

16 

17 Returns: 

18 Exit code (0 = all match, 1 = mismatches found) 

19 """ 

20 from little_loops.doc_counts import ( 

21 fix_counts, 

22 format_result_json, 

23 format_result_markdown, 

24 format_result_text, 

25 verify_documentation, 

26 ) 

27 

28 parser = argparse.ArgumentParser( 

29 prog="ll-verify-docs", 

30 description="Verify documented counts match actual file counts", 

31 formatter_class=argparse.RawDescriptionHelpFormatter, 

32 epilog=""" 

33Examples: 

34 %(prog)s # Check and show results 

35 %(prog)s --json # Output as JSON 

36 %(prog)s --format markdown # Markdown report 

37 %(prog)s --fix # Auto-fix mismatches 

38 

39Exit codes: 

40 0 - All counts match 

41 1 - Mismatches found 

42 2 - Error occurred 

43""", 

44 ) 

45 

46 parser.add_argument( 

47 "-j", 

48 "--json", 

49 action="store_true", 

50 help="Output as JSON", 

51 ) 

52 

53 parser.add_argument( 

54 "-f", 

55 "--format", 

56 choices=["text", "json", "markdown"], 

57 default="text", 

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

59 ) 

60 

61 parser.add_argument( 

62 "--fix", 

63 action="store_true", 

64 help="Auto-fix count mismatches in documentation files", 

65 ) 

66 

67 parser.add_argument( 

68 "-C", 

69 "--directory", 

70 type=Path, 

71 default=None, 

72 help="Base directory (default: current directory)", 

73 ) 

74 

75 args = parser.parse_args() 

76 

77 configure_output() 

78 logger = Logger(use_color=use_color_enabled()) 

79 

80 # Determine base directory 

81 base_dir = args.directory or Path.cwd() 

82 

83 # Run verification 

84 result = verify_documentation(base_dir) 

85 

86 # Format output 

87 if args.json or args.format == "json": 

88 output = format_result_json(result) 

89 elif args.format == "markdown": 

90 output = format_result_markdown(result) 

91 else: 

92 output = format_result_text(result) 

93 

94 print(output) 

95 

96 # Auto-fix if requested 

97 if args.fix and not result.all_match: 

98 fix_result = fix_counts(base_dir, result) 

99 logger.success( 

100 f"Fixed {fix_result.fixed_count} count(s) in {len(fix_result.files_modified)} file(s)" 

101 ) 

102 

103 # Return exit code based on results 

104 return 0 if result.all_match else 1 

105 

106 

107def main_verify_skill_budget() -> int: 

108 """Entry point for ll-verify-skill-budget command. 

109 

110 Scan skill description tokens and fail if total exceeds the configured budget. 

111 

112 Returns: 

113 Exit code (0 = under budget, 1 = over budget) 

114 """ 

115 from little_loops.doc_counts import ( 

116 _DEFAULT_BUDGET_TOKENS, 

117 check_skill_budget, 

118 ) 

119 

120 parser = argparse.ArgumentParser( 

121 prog="ll-verify-skill-budget", 

122 description="Verify skill description token footprint stays within listing budget", 

123 formatter_class=argparse.RawDescriptionHelpFormatter, 

124 epilog=""" 

125Examples: 

126 %(prog)s # Check against default 2000-token budget 

127 %(prog)s --threshold 1500 # Custom token threshold 

128 

129Exit codes: 

130 0 - Under budget 

131 1 - Over budget 

132""", 

133 ) 

134 

135 parser.add_argument( 

136 "--threshold", 

137 type=int, 

138 default=None, 

139 metavar="N", 

140 help=f"Token budget threshold (default: {_DEFAULT_BUDGET_TOKENS}; overrides ll-config.json)", 

141 ) 

142 

143 parser.add_argument( 

144 "-C", 

145 "--directory", 

146 type=Path, 

147 default=None, 

148 help="Base directory (default: current directory)", 

149 ) 

150 

151 args = parser.parse_args() 

152 

153 configure_output() 

154 logger = Logger(use_color=use_color_enabled()) 

155 

156 base_dir = args.directory or Path.cwd() 

157 

158 # Resolve threshold: CLI arg > config file > default 

159 threshold = args.threshold 

160 if threshold is None: 

161 try: 

162 from little_loops.config import BRConfig 

163 

164 threshold = ( 

165 BRConfig(base_dir) 

166 ._raw_config.get("skill_budget", {}) 

167 .get("threshold_tokens", _DEFAULT_BUDGET_TOKENS) 

168 ) 

169 except Exception: 

170 threshold = _DEFAULT_BUDGET_TOKENS 

171 

172 result = check_skill_budget(base_dir=base_dir, threshold_tokens=threshold) 

173 

174 # Per-skill breakdown header 

175 print("Skill Description Token Budget Check") 

176 print("=" * 40) 

177 print(f"Threshold: {result.threshold_tokens} tokens") 

178 print() 

179 

180 if result.skill_breakdown: 

181 print(f"{'Tokens':>6} Skill") 

182 print(f"{'------':>6} {'-----'}") 

183 for skill_md, _, tokens in result.skill_breakdown: 

184 marker = " !" if tokens >= 200 else " " 

185 print(f"{tokens:>6}{marker} {skill_md.parent.name}") 

186 print() 

187 

188 if result.under_budget: 

189 logger.success( 

190 f"Total: {result.total_tokens} / {result.threshold_tokens} tokens — under budget" 

191 ) 

192 return 0 

193 else: 

194 logger.error( 

195 f"Total: {result.total_tokens} / {result.threshold_tokens} tokens — OVER BUDGET" 

196 ) 

197 if result.violations: 

198 print("\nTop contributors (≥200 tokens each):") 

199 for skill_md, _, tokens in result.violations: 

200 print(f" {tokens:>6} {skill_md.parent.name}") 

201 return 1 

202 

203 

204def main_check_links() -> int: 

205 """Entry point for ll-check-links command. 

206 

207 Check markdown documentation for broken links. 

208 

209 Returns: 

210 Exit code (0 = all links valid, 1 = broken links found, 2 = error) 

211 """ 

212 from little_loops.link_checker import ( 

213 check_markdown_links, 

214 format_result_json, 

215 format_result_markdown, 

216 format_result_text, 

217 load_ignore_patterns, 

218 ) 

219 

220 parser = argparse.ArgumentParser( 

221 prog="ll-check-links", 

222 description="Check markdown documentation for broken links", 

223 formatter_class=argparse.RawDescriptionHelpFormatter, 

224 epilog=""" 

225Examples: 

226 %(prog)s # Check all markdown files 

227 %(prog)s --json # Output as JSON 

228 %(prog)s --format markdown # Markdown report 

229 %(prog)s docs/ # Check specific directory 

230 %(prog)s --ignore 'http://localhost.*' # Ignore pattern 

231 

232Exit codes: 

233 0 - All links valid 

234 1 - Broken links found 

235 2 - Error occurred 

236""", 

237 ) 

238 

239 parser.add_argument( 

240 "-j", 

241 "--json", 

242 action="store_true", 

243 help="Output as JSON", 

244 ) 

245 

246 parser.add_argument( 

247 "-f", 

248 "--format", 

249 choices=["text", "json", "markdown"], 

250 default="text", 

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

252 ) 

253 

254 parser.add_argument( 

255 "-C", 

256 "--directory", 

257 type=Path, 

258 default=None, 

259 help="Base directory (default: current directory)", 

260 ) 

261 

262 parser.add_argument( 

263 "--ignore", 

264 action="append", 

265 default=[], 

266 help="Ignore URL patterns (can be used multiple times)", 

267 ) 

268 

269 parser.add_argument( 

270 "--timeout", 

271 "-t", 

272 type=int, 

273 default=10, 

274 help="Request timeout in seconds (default: 10)", 

275 ) 

276 

277 parser.add_argument( 

278 "-w", 

279 "--workers", 

280 type=int, 

281 default=10, 

282 help="Maximum concurrent HTTP requests (default: 10)", 

283 ) 

284 

285 parser.add_argument( 

286 "-v", 

287 "--verbose", 

288 action="store_true", 

289 help="Show verbose output", 

290 ) 

291 

292 args = parser.parse_args() 

293 

294 configure_output() 

295 

296 # Determine base directory 

297 base_dir = args.directory or Path.cwd() 

298 

299 # Load ignore patterns from config + CLI args 

300 ignore_patterns = load_ignore_patterns(base_dir) 

301 ignore_patterns.extend(args.ignore) 

302 

303 # Run link check 

304 result = check_markdown_links( 

305 base_dir, ignore_patterns, args.timeout, args.verbose, args.workers 

306 ) 

307 

308 # Format output 

309 if args.json or args.format == "json": 

310 output = format_result_json(result) 

311 elif args.format == "markdown": 

312 output = format_result_markdown(result) 

313 else: 

314 output = format_result_text(result) 

315 

316 print(output) 

317 

318 # Return exit code based on results 

319 if result.has_errors: 

320 return 1 

321 return 0