Coverage for little_loops / cli / sprint / edit.py: 9%

92 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-18 16:18 -0500

1"""ll-sprint edit subcommand.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6from pathlib import Path 

7 

8from little_loops.cli.sprint._helpers import _build_issue_contents, _render_dependency_analysis 

9from little_loops.cli_args import parse_issue_ids 

10from little_loops.logger import Logger 

11from little_loops.sprint import SprintManager 

12 

13 

14def _cmd_sprint_edit(args: argparse.Namespace, manager: SprintManager) -> int: 

15 """Edit a sprint's issue list.""" 

16 import re 

17 

18 logger = Logger() 

19 sprint = manager.load(args.sprint) 

20 if not sprint: 

21 logger.error(f"Sprint not found: {args.sprint}") 

22 return 1 

23 

24 if not args.add and not args.remove and not args.prune and not args.revalidate: 

25 logger.error("No edit flags specified. Use --add, --remove, --prune, or --revalidate.") 

26 return 1 

27 

28 original_issues = list(sprint.issues) 

29 changed = False 

30 

31 # --add: add new issue IDs 

32 if args.add: 

33 add_ids = parse_issue_ids(args.add) 

34 if add_ids: 

35 valid = manager.validate_issues(list(add_ids)) 

36 invalid = add_ids - set(valid.keys()) 

37 if invalid: 

38 logger.warning(f"Issue IDs not found (skipping): {', '.join(sorted(invalid))}") 

39 

40 existing = set(sprint.issues) 

41 added = [] 

42 for issue_id in sorted(valid.keys()): 

43 if issue_id not in existing: 

44 sprint.issues.append(issue_id) 

45 added.append(issue_id) 

46 else: 

47 logger.info(f"Already in sprint: {issue_id}") 

48 if added: 

49 logger.success(f"Added: {', '.join(added)}") 

50 changed = True 

51 

52 # --remove: remove issue IDs 

53 if args.remove: 

54 remove_ids = parse_issue_ids(args.remove) 

55 if remove_ids: 

56 before = len(sprint.issues) 

57 sprint.issues = [i for i in sprint.issues if i not in remove_ids] 

58 removed_count = before - len(sprint.issues) 

59 not_found = remove_ids - set(original_issues) 

60 if not_found: 

61 logger.warning(f"Not in sprint: {', '.join(sorted(not_found))}") 

62 if removed_count > 0: 

63 logger.success(f"Removed {removed_count} issue(s)") 

64 changed = True 

65 

66 # --prune: remove invalid and completed references 

67 if args.prune: 

68 valid = manager.validate_issues(sprint.issues) 

69 invalid_ids = set(sprint.issues) - set(valid.keys()) 

70 

71 # Also detect completed issues 

72 completed_ids: set[str] = set() 

73 if manager.config: 

74 completed_dir = manager.config.get_completed_dir() 

75 if completed_dir.exists(): 

76 for path in completed_dir.glob("*.md"): 

77 match = re.search(r"(BUG|FEAT|ENH)-(\d+)", path.name) 

78 if match: 

79 completed_ids.add(f"{match.group(1)}-{match.group(2)}") 

80 

81 prune_ids = invalid_ids | (completed_ids & set(sprint.issues)) 

82 if prune_ids: 

83 sprint.issues = [i for i in sprint.issues if i not in prune_ids] 

84 pruned_invalid = invalid_ids & prune_ids 

85 pruned_completed = (completed_ids & set(original_issues)) - invalid_ids 

86 if pruned_invalid: 

87 logger.success(f"Pruned invalid: {', '.join(sorted(pruned_invalid))}") 

88 if pruned_completed: 

89 logger.success(f"Pruned completed: {', '.join(sorted(pruned_completed))}") 

90 changed = True 

91 else: 

92 logger.info("Nothing to prune — all issues are valid and active") 

93 

94 # Save if changed 

95 if changed: 

96 sprint.save(manager.sprints_dir) 

97 logger.success(f"Saved {args.sprint} ({len(sprint.issues)} issues)") 

98 if original_issues != sprint.issues: 

99 logger.info(f" Was: {', '.join(original_issues)}") 

100 logger.info(f" Now: {', '.join(sprint.issues)}") 

101 

102 # --revalidate: re-run dependency analysis 

103 if args.revalidate: 

104 valid = manager.validate_issues(sprint.issues) 

105 issue_infos = manager.load_issue_infos(list(valid.keys())) 

106 if issue_infos: 

107 from little_loops.dependency_mapper import ( 

108 analyze_dependencies, 

109 gather_all_issue_ids, 

110 ) 

111 

112 _config = manager.config 

113 _issues_dir = ( 

114 _config.project_root / _config.issues.base_dir if _config else Path(".issues") 

115 ) 

116 _all_known_ids = gather_all_issue_ids(_issues_dir, config=_config) 

117 issue_contents = _build_issue_contents(issue_infos) 

118 dep_report = analyze_dependencies( 

119 issue_infos, issue_contents, all_known_ids=_all_known_ids 

120 ) 

121 _render_dependency_analysis(dep_report, logger) 

122 else: 

123 logger.info("No valid issues to analyze") 

124 

125 invalid = set(sprint.issues) - set(valid.keys()) 

126 if invalid: 

127 logger.warning(f"{len(invalid)} issue(s) not found: {', '.join(sorted(invalid))}") 

128 

129 return 0