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
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""ll-sprint edit subcommand."""
3from __future__ import annotations
5import argparse
6from pathlib import Path
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
14def _cmd_sprint_edit(args: argparse.Namespace, manager: SprintManager) -> int:
15 """Edit a sprint's issue list."""
16 import re
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
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
28 original_issues = list(sprint.issues)
29 changed = False
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))}")
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
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
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())
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)}")
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")
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)}")
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 )
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")
125 invalid = set(sprint.issues) - set(valid.keys())
126 if invalid:
127 logger.warning(f"{len(invalid)} issue(s) not found: {', '.join(sorted(invalid))}")
129 return 0