Coverage for little_loops / cli / sprint / edit.py: 84%
85 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-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 logger = Logger()
17 sprint = manager.load(args.sprint)
18 if not sprint:
19 logger.error(f"Sprint not found: {args.sprint}")
20 return 1
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
26 original_issues = list(sprint.issues)
27 changed = False
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))}")
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
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
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())
69 # Also detect completed issues via frontmatter status (ENH-1424)
70 from little_loops.frontmatter import parse_frontmatter
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 }
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")
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)}")
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 )
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")
123 invalid = set(sprint.issues) - set(valid.keys())
124 if invalid:
125 logger.warning(f"{len(invalid)} issue(s) not found: {', '.join(sorted(invalid))}")
127 return 0