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

2

3 from __future__ import annotations

4

5 import argparse

6 from pathlib import Path

7

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

9 from little_loops.cli_args import parse_issue_ids

10 from little_loops.logger import Logger

11 from little_loops.sprint import SprintManager

12

13

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

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

16 logger = Logger()

17 sprint = manager.load_or_resolve(args.sprint)

18 if not sprint:

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

20 return 1

21

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

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

24 return 1

25

26 original_issues = list(sprint.issues)

27 changed = False

28

29 # --add: add new issue IDs

30 if args.add:

31 add_ids = parse_issue_ids(args.add)

32 if add_ids:

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

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

35 if invalid:

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

37

38 existing = set(sprint.issues)

39 added = []

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

41 if issue_id not in existing:

42 sprint.issues.append(issue_id)

43 added.append(issue_id)

44 else:

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

46 if added:

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

48 changed = True

49

50 # --remove: remove issue IDs

51 if args.remove:

52 remove_ids = parse_issue_ids(args.remove)

53 if remove_ids:

54 before = len(sprint.issues)

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

56 removed_count = before - len(sprint.issues)

57 not_found = remove_ids - set(original_issues)

58 if not_found:

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

60 if removed_count > 0:

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

62 changed = True

63

64 # --prune: remove invalid and completed references

65 if args.prune:

66 valid = manager.validate_issues(sprint.issues)

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

68

69 # Also detect completed issues via frontmatter status (ENH-1424)

70 from little_loops.frontmatter import parse_frontmatter

71

72 completed_ids = {

73 issue_id

74 for issue_id, path in valid.items()

75 if parse_frontmatter(path.read_text(encoding="utf-8")).get("status", "open")

76 in ("done", "cancelled")

77 }

78

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

80 if prune_ids:

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

82 pruned_invalid = invalid_ids & prune_ids

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

84 if pruned_invalid:

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

86 if pruned_completed:

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

88 changed = True

89 else:

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

91

92 # Save if changed

93 if changed:

94 sprint.save(manager.sprints_dir)

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

96 if original_issues != sprint.issues:

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

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

99

100 # --revalidate: re-run dependency analysis

101 if args.revalidate:

102 valid = manager.validate_issues(sprint.issues)

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

104 if issue_infos:

105 from little_loops.dependency_mapper import (

106 analyze_dependencies,

107 gather_all_issue_ids,

108 )

109

110 _config = manager.config

111 _issues_dir = (

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

113 )

114 _all_known_ids = gather_all_issue_ids(_issues_dir, config=_config)

115 issue_contents = _build_issue_contents(issue_infos)

116 dep_report = analyze_dependencies(

117 issue_infos, issue_contents, all_known_ids=_all_known_ids

118 )

119 _render_dependency_analysis(dep_report, logger)

120 else:

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

122

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

124 if invalid:

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

126

127 return 0