Coverage for little_loops / cli / docs.py: 10%
121 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-16 13:11 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-16 13:11 -0500
1"""ll-verify-docs and ll-check-links: Documentation verification commands."""
3from __future__ import annotations
5import argparse
6import sys
7from pathlib import Path
9from little_loops.cli.output import configure_output, print_json, use_color_enabled
10from little_loops.cli_args import add_json_arg
11from little_loops.logger import Logger
12from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context
15def main_verify_docs() -> int:
16 """Entry point for ll-verify-docs command.
18 Verify that documented counts (commands, agents, skills) match actual file counts.
20 Returns:
21 Exit code (0 = all match, 1 = mismatches found)
22 """
23 with cli_event_context(DEFAULT_DB_PATH, "ll-verify-docs", sys.argv[1:]):
24 from little_loops.doc_counts import (
25 fix_counts,
26 format_result_json,
27 format_result_markdown,
28 format_result_text,
29 verify_documentation,
30 )
32 parser = argparse.ArgumentParser(
33 prog="ll-verify-docs",
34 description="Verify documented counts match actual file counts",
35 formatter_class=argparse.RawDescriptionHelpFormatter,
36 epilog="""
37Examples:
38 %(prog)s # Check and show results
39 %(prog)s --json # Output as JSON
40 %(prog)s --format markdown # Markdown report
41 %(prog)s --fix # Auto-fix mismatches
43Exit codes:
44 0 - All counts match
45 1 - Mismatches found
46 2 - Error occurred
47""",
48 )
50 parser.add_argument(
51 "-j",
52 "--json",
53 action="store_true",
54 help="Output as JSON",
55 )
57 parser.add_argument(
58 "-f",
59 "--format",
60 choices=["text", "json", "markdown"],
61 default="text",
62 help="Output format (default: text)",
63 )
65 parser.add_argument(
66 "--fix",
67 action="store_true",
68 help="Auto-fix count mismatches in documentation files",
69 )
71 parser.add_argument(
72 "-C",
73 "--directory",
74 type=Path,
75 default=None,
76 help="Base directory (default: current directory)",
77 )
79 args = parser.parse_args()
81 configure_output()
82 logger = Logger(use_color=use_color_enabled())
84 # Determine base directory
85 base_dir = args.directory or Path.cwd()
87 # Run verification
88 result = verify_documentation(base_dir)
90 # Format output
91 if args.json or args.format == "json":
92 output = format_result_json(result)
93 elif args.format == "markdown":
94 output = format_result_markdown(result)
95 else:
96 output = format_result_text(result)
98 print(output)
100 # Auto-fix if requested
101 if args.fix and not result.all_match:
102 fix_result = fix_counts(base_dir, result)
103 logger.success(
104 f"Fixed {fix_result.fixed_count} count(s) in {len(fix_result.files_modified)} file(s)"
105 )
107 # Return exit code based on results
108 return 0 if result.all_match else 1
111def main_verify_skill_budget() -> int:
112 """Entry point for ll-verify-skill-budget command.
114 Scan skill description tokens and fail if total exceeds the configured budget.
116 Returns:
117 Exit code (0 = under budget, 1 = over budget)
118 """
119 with cli_event_context(DEFAULT_DB_PATH, "ll-verify-skill-budget", sys.argv[1:]):
120 from little_loops.doc_counts import (
121 _DEFAULT_BUDGET_TOKENS,
122 check_skill_budget,
123 )
125 parser = argparse.ArgumentParser(
126 prog="ll-verify-skill-budget",
127 description="Verify skill description token footprint stays within listing budget",
128 formatter_class=argparse.RawDescriptionHelpFormatter,
129 epilog="""
130Examples:
131 %(prog)s # Check against default 2000-token budget
132 %(prog)s --threshold 1500 # Custom token threshold
134Exit codes:
135 0 - Under budget
136 1 - Over budget
137""",
138 )
140 parser.add_argument(
141 "--threshold",
142 type=int,
143 default=None,
144 metavar="N",
145 help=f"Token budget threshold (default: {_DEFAULT_BUDGET_TOKENS}; overrides ll-config.json)",
146 )
148 parser.add_argument(
149 "-C",
150 "--directory",
151 type=Path,
152 default=None,
153 help="Base directory (default: current directory)",
154 )
156 add_json_arg(parser)
158 args = parser.parse_args()
160 configure_output()
161 logger = Logger(use_color=use_color_enabled())
163 base_dir = args.directory or Path.cwd()
165 # Resolve threshold: CLI arg > config file > default
166 threshold = args.threshold
167 if threshold is None:
168 try:
169 from little_loops.config import BRConfig
171 threshold = (
172 BRConfig(base_dir)
173 ._raw_config.get("skill_budget", {})
174 .get("threshold_tokens", _DEFAULT_BUDGET_TOKENS)
175 )
176 except Exception:
177 threshold = _DEFAULT_BUDGET_TOKENS
179 result = check_skill_budget(base_dir=base_dir, threshold_tokens=threshold)
181 if args.json:
182 print_json(
183 {
184 "under_budget": result.under_budget,
185 "total_tokens": result.total_tokens,
186 "threshold_tokens": result.threshold_tokens,
187 "skills": [
188 {
189 "name": path.parent.name,
190 "char_count": len(desc),
191 "token_estimate": tokens,
192 }
193 for path, desc, tokens in result.skill_breakdown
194 ],
195 "violations": [
196 {
197 "name": path.parent.name,
198 "char_count": len(desc),
199 "token_estimate": tokens,
200 }
201 for path, desc, tokens in result.violations
202 ],
203 }
204 )
205 return 0 if result.under_budget else 1
207 # Per-skill breakdown header
208 print("Skill Description Token Budget Check")
209 print("=" * 40)
210 print(f"Threshold: {result.threshold_tokens} tokens")
211 print()
213 if result.skill_breakdown:
214 print(f"{'Tokens':>6} Skill")
215 print(f"{'------':>6} {'-----'}")
216 for skill_md, _, tokens in result.skill_breakdown:
217 marker = " !" if tokens >= 200 else " "
218 print(f"{tokens:>6}{marker} {skill_md.parent.name}")
219 print()
221 if result.under_budget:
222 logger.success(
223 f"Total: {result.total_tokens} / {result.threshold_tokens} tokens — under budget"
224 )
225 return 0
226 else:
227 logger.error(
228 f"Total: {result.total_tokens} / {result.threshold_tokens} tokens — OVER BUDGET"
229 )
230 if result.violations:
231 print("\nTop contributors (≥200 tokens each):")
232 for skill_md, _, tokens in result.violations:
233 print(f" {tokens:>6} {skill_md.parent.name}")
234 return 1
237def main_verify_skills() -> int:
238 """Entry point for ll-verify-skills command.
240 Scan SKILL.md files and fail if any exceed the 500-line limit.
242 Returns:
243 Exit code (0 = all within limit, 1 = violations found)
244 """
245 with cli_event_context(DEFAULT_DB_PATH, "ll-verify-skills", sys.argv[1:]):
246 from little_loops.doc_counts import check_skill_sizes
248 parser = argparse.ArgumentParser(
249 prog="ll-verify-skills",
250 description="Verify no SKILL.md file exceeds the 500-line limit",
251 formatter_class=argparse.RawDescriptionHelpFormatter,
252 epilog="""
253Examples:
254 %(prog)s # Check against default 500-line limit
255 %(prog)s --limit 400 # Custom line limit
256 %(prog)s --json # Output as JSON
258Exit codes:
259 0 - All SKILL.md files within limit
260 1 - One or more violations found
261""",
262 )
264 parser.add_argument(
265 "--limit",
266 type=int,
267 default=500,
268 metavar="N",
269 help="Maximum allowed lines per SKILL.md (default: 500)",
270 )
272 parser.add_argument(
273 "-C",
274 "--directory",
275 type=Path,
276 default=None,
277 help="Base directory (default: current directory)",
278 )
280 add_json_arg(parser)
282 args = parser.parse_args()
284 configure_output()
285 logger = Logger(use_color=use_color_enabled())
287 base_dir = args.directory or Path.cwd()
289 violations = check_skill_sizes(base_dir=base_dir, limit=args.limit)
291 if args.json:
292 print_json(
293 {
294 "ok": len(violations) == 0,
295 "limit": args.limit,
296 "violations": [
297 {"name": path.parent.name, "lines": lines} for path, lines in violations
298 ],
299 }
300 )
301 return 0 if not violations else 1
303 if not violations:
304 logger.success(f"All SKILL.md files within {args.limit}-line limit")
305 return 0
307 logger.error(f"{len(violations)} SKILL.md file(s) exceed the {args.limit}-line limit:")
308 for path, lines in violations:
309 print(f" {lines:>6} lines {path.parent.name}/SKILL.md")
310 return 1
313def main_check_links() -> int:
314 """Entry point for ll-check-links command.
316 Check markdown documentation for broken links.
318 Returns:
319 Exit code (0 = all links valid, 1 = broken links found, 2 = error)
320 """
321 with cli_event_context(DEFAULT_DB_PATH, "ll-check-links", sys.argv[1:]):
322 from little_loops.link_checker import (
323 check_markdown_links,
324 format_result_json,
325 format_result_markdown,
326 format_result_text,
327 load_ignore_patterns,
328 )
330 parser = argparse.ArgumentParser(
331 prog="ll-check-links",
332 description="Check markdown documentation for broken links",
333 formatter_class=argparse.RawDescriptionHelpFormatter,
334 epilog="""
335Examples:
336 %(prog)s # Check all markdown files
337 %(prog)s --json # Output as JSON
338 %(prog)s --format markdown # Markdown report
339 %(prog)s docs/ # Check specific directory
340 %(prog)s --ignore 'http://localhost.*' # Ignore pattern
342Exit codes:
343 0 - All links valid
344 1 - Broken links found
345 2 - Error occurred
346""",
347 )
349 parser.add_argument(
350 "-j",
351 "--json",
352 action="store_true",
353 help="Output as JSON",
354 )
356 parser.add_argument(
357 "-f",
358 "--format",
359 choices=["text", "json", "markdown"],
360 default="text",
361 help="Output format (default: text)",
362 )
364 parser.add_argument(
365 "-C",
366 "--directory",
367 type=Path,
368 default=None,
369 help="Base directory (default: current directory)",
370 )
372 parser.add_argument(
373 "--ignore",
374 action="append",
375 default=[],
376 help="Ignore URL patterns (can be used multiple times)",
377 )
379 parser.add_argument(
380 "--timeout",
381 "-t",
382 type=int,
383 default=10,
384 help="Request timeout in seconds (default: 10)",
385 )
387 parser.add_argument(
388 "-w",
389 "--workers",
390 type=int,
391 default=10,
392 help="Maximum concurrent HTTP requests (default: 10)",
393 )
395 parser.add_argument(
396 "-v",
397 "--verbose",
398 action="store_true",
399 help="Show verbose output",
400 )
402 args = parser.parse_args()
404 configure_output()
406 # Determine base directory
407 base_dir = args.directory or Path.cwd()
409 # Load ignore patterns from config + CLI args
410 ignore_patterns = load_ignore_patterns(base_dir)
411 ignore_patterns.extend(args.ignore)
413 # Run link check
414 result = check_markdown_links(
415 base_dir, ignore_patterns, args.timeout, args.verbose, args.workers
416 )
418 # Format output
419 if args.json or args.format == "json":
420 output = format_result_json(result)
421 elif args.format == "markdown":
422 output = format_result_markdown(result)
423 else:
424 output = format_result_text(result)
426 print(output)
428 # Return exit code based on results
429 if result.has_errors:
430 return 1
431 return 0