Coverage for little_loops / cli / issues / sequence.py: 89%

38 statements  

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

1"""ll-issues sequence: Suggest dependency-ordered implementation sequence.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6from typing import TYPE_CHECKING 

7 

8from little_loops.cli.output import PRIORITY_COLOR, TYPE_COLOR, colorize, print_json 

9 

10if TYPE_CHECKING: 

11 from little_loops.config import BRConfig 

12 

13 

14def cmd_sequence(config: BRConfig, args: argparse.Namespace) -> int: 

15 """Output a dependency-ordered list of active issues with rationale. 

16 

17 Args: 

18 config: Project configuration 

19 args: Parsed arguments with .limit and optional .type attributes 

20 

21 Returns: 

22 Exit code (0 = success) 

23 """ 

24 from little_loops.dependency_graph import DependencyGraph 

25 from little_loops.issue_parser import find_issues 

26 

27 type_prefixes = {args.type} if getattr(args, "type", None) else None 

28 issues = find_issues(config, type_prefixes=type_prefixes) 

29 

30 if not issues: 

31 print("No active issues found.") 

32 return 0 

33 

34 graph = DependencyGraph.from_issues(issues) 

35 

36 try: 

37 ordered = graph.topological_sort() 

38 except ValueError as exc: 

39 print(f"Warning: dependency cycle detected — {exc}") 

40 ordered = issues # fall back to priority order 

41 

42 limit = args.limit 

43 shown = ordered[:limit] 

44 

45 if getattr(args, "json", False): 

46 type_filter = getattr(args, "type", None) 

47 print_json( 

48 [ 

49 { 

50 "id": issue.issue_id, 

51 "priority": issue.priority, 

52 "title": issue.title, 

53 "path": str(issue.path), 

54 "blocked_by": sorted(graph.blocked_by.get(issue.issue_id, set())), 

55 "blocks": issue.blocks, 

56 **({"type_filter": type_filter} if type_filter else {}), 

57 } 

58 for issue in shown 

59 ] 

60 ) 

61 return 0 

62 

63 print(f"Suggested implementation sequence ({len(shown)} of {len(ordered)} issues):\n") 

64 for issue in shown: 

65 blockers = graph.blocked_by.get(issue.issue_id, set()) 

66 if blockers: 

67 rationale = f"blocked by: {', '.join(sorted(blockers))}" 

68 else: 

69 rationale = "no blockers" 

70 issue_prefix = issue.issue_id.split("-", 1)[0] 

71 colored_id = colorize(issue.issue_id, TYPE_COLOR.get(issue_prefix, "0")) 

72 colored_pri = colorize(issue.priority, PRIORITY_COLOR.get(issue.priority, "0")) 

73 print(f" [{colored_pri}, {rationale}] {colored_id}: {issue.title}") 

74 

75 if len(ordered) > limit: 

76 remaining = len(ordered) - limit 

77 print(f"\n … +{remaining} more (use --limit to show more)") 

78 

79 return 0