Coverage for little_loops / cli / migrate_status.py: 17%
64 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-16 13:11 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-16 13:11 -0500
1"""ll-migrate-status: Normalize non-canonical status: values in issue files to canonical ones."""
3from __future__ import annotations
5import argparse
6import re
7import sys
8from pathlib import Path
10from little_loops.cli_args import add_config_arg, add_dry_run_arg
11from little_loops.frontmatter import STATUS_SYNONYMS
12from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context
14_STATUS_FIELD_RE = re.compile(r"^(status: )(.+)$", re.MULTILINE)
17def _migrate_content(content: str) -> tuple[str, list[str]]:
18 """Normalize status field in frontmatter. Returns (updated_content, list_of_changes)."""
19 changes: list[str] = []
21 def _replace(m: re.Match[str]) -> str:
22 prefix, value = m.group(1), m.group(2)
23 canonical = STATUS_SYNONYMS.get(value, value)
24 if canonical != value:
25 changes.append(f"status: {value!r} → status: {canonical!r}")
26 return f"{prefix}{canonical}"
27 return m.group(0)
29 updated = _STATUS_FIELD_RE.sub(_replace, content)
30 return updated, changes
33def main_migrate_status() -> int:
34 """Entry point for ll-migrate-status command.
36 Normalizes non-canonical status: values in all .issues/**/*.md files to their
37 canonical equivalents using STATUS_SYNONYMS from frontmatter.py.
39 Returns:
40 Exit code (0 = success, 1 = error)
41 """
42 with cli_event_context(DEFAULT_DB_PATH, "ll-migrate-status", sys.argv[1:]):
43 parser = argparse.ArgumentParser(
44 prog="ll-migrate-status",
45 description=(
46 "Normalize non-canonical status: frontmatter values to canonical ones "
47 "(e.g. 'completed' → 'done', 'wip' → 'in_progress'). "
48 "One-time migration for ENH-1551."
49 ),
50 formatter_class=argparse.RawDescriptionHelpFormatter,
51 epilog="""
52Examples:
53 %(prog)s --dry-run # Preview all planned normalizations (strongly advised first)
54 %(prog)s # Execute migration
55""",
56 )
57 add_dry_run_arg(parser)
58 add_config_arg(parser)
59 args = parser.parse_args()
61 dry_run: bool = args.dry_run
62 repo_root: Path = args.config or Path.cwd()
64 issues_dir = repo_root / ".issues"
65 if not issues_dir.exists():
66 print(f"No .issues/ directory found at {repo_root}")
67 return 1
69 if dry_run:
70 print("[DRY RUN] No files will be modified.")
72 normalized = 0
73 errors: list[str] = []
75 for file_path in sorted(issues_dir.rglob("*.md")):
76 try:
77 content = file_path.read_text(encoding="utf-8")
78 except OSError as exc:
79 errors.append(str(file_path))
80 print(f" [ERROR] {file_path}: {exc}")
81 continue
83 updated, changes = _migrate_content(content)
84 if not changes:
85 continue
87 prefix = "[DRY RUN] " if dry_run else ""
88 rel = file_path.relative_to(repo_root)
89 for change in changes:
90 print(f" {prefix}NORMALIZE {rel}: {change}")
92 if not dry_run:
93 try:
94 file_path.write_text(updated, encoding="utf-8")
95 normalized += 1
96 except OSError as exc:
97 errors.append(str(file_path))
98 print(f" [ERROR] {file_path}: {exc}")
99 else:
100 normalized += 1
102 print()
103 print(f"Results: {normalized} files {'would be ' if dry_run else ''}updated.")
104 if errors:
105 print(f" Errors: {len(errors)}")
106 return 1
107 return 0