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
« 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."""
3from __future__ import annotations
5import argparse
6from pathlib import Path
8from little_loops.cli.output import configure_output, use_color_enabled
9from little_loops.logger import Logger
12def main_verify_docs() -> int:
13 """Entry point for ll-verify-docs command.
15 Verify that documented counts (commands, agents, skills) match actual file counts.
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 )
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
39Exit codes:
40 0 - All counts match
41 1 - Mismatches found
42 2 - Error occurred
43""",
44 )
46 parser.add_argument(
47 "-j",
48 "--json",
49 action="store_true",
50 help="Output as JSON",
51 )
53 parser.add_argument(
54 "-f",
55 "--format",
56 choices=["text", "json", "markdown"],
57 default="text",
58 help="Output format (default: text)",
59 )
61 parser.add_argument(
62 "--fix",
63 action="store_true",
64 help="Auto-fix count mismatches in documentation files",
65 )
67 parser.add_argument(
68 "-C",
69 "--directory",
70 type=Path,
71 default=None,
72 help="Base directory (default: current directory)",
73 )
75 args = parser.parse_args()
77 configure_output()
78 logger = Logger(use_color=use_color_enabled())
80 # Determine base directory
81 base_dir = args.directory or Path.cwd()
83 # Run verification
84 result = verify_documentation(base_dir)
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)
94 print(output)
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 )
103 # Return exit code based on results
104 return 0 if result.all_match else 1
107def main_verify_skill_budget() -> int:
108 """Entry point for ll-verify-skill-budget command.
110 Scan skill description tokens and fail if total exceeds the configured budget.
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 )
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
129Exit codes:
130 0 - Under budget
131 1 - Over budget
132""",
133 )
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 )
143 parser.add_argument(
144 "-C",
145 "--directory",
146 type=Path,
147 default=None,
148 help="Base directory (default: current directory)",
149 )
151 args = parser.parse_args()
153 configure_output()
154 logger = Logger(use_color=use_color_enabled())
156 base_dir = args.directory or Path.cwd()
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
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
172 result = check_skill_budget(base_dir=base_dir, threshold_tokens=threshold)
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()
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()
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
204def main_check_links() -> int:
205 """Entry point for ll-check-links command.
207 Check markdown documentation for broken links.
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 )
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
232Exit codes:
233 0 - All links valid
234 1 - Broken links found
235 2 - Error occurred
236""",
237 )
239 parser.add_argument(
240 "-j",
241 "--json",
242 action="store_true",
243 help="Output as JSON",
244 )
246 parser.add_argument(
247 "-f",
248 "--format",
249 choices=["text", "json", "markdown"],
250 default="text",
251 help="Output format (default: text)",
252 )
254 parser.add_argument(
255 "-C",
256 "--directory",
257 type=Path,
258 default=None,
259 help="Base directory (default: current directory)",
260 )
262 parser.add_argument(
263 "--ignore",
264 action="append",
265 default=[],
266 help="Ignore URL patterns (can be used multiple times)",
267 )
269 parser.add_argument(
270 "--timeout",
271 "-t",
272 type=int,
273 default=10,
274 help="Request timeout in seconds (default: 10)",
275 )
277 parser.add_argument(
278 "-w",
279 "--workers",
280 type=int,
281 default=10,
282 help="Maximum concurrent HTTP requests (default: 10)",
283 )
285 parser.add_argument(
286 "-v",
287 "--verbose",
288 action="store_true",
289 help="Show verbose output",
290 )
292 args = parser.parse_args()
294 configure_output()
296 # Determine base directory
297 base_dir = args.directory or Path.cwd()
299 # Load ignore patterns from config + CLI args
300 ignore_patterns = load_ignore_patterns(base_dir)
301 ignore_patterns.extend(args.ignore)
303 # Run link check
304 result = check_markdown_links(
305 base_dir, ignore_patterns, args.timeout, args.verbose, args.workers
306 )
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)
316 print(output)
318 # Return exit code based on results
319 if result.has_errors:
320 return 1
321 return 0