Coverage for src/workstack/status/collectors/plan.py: 100%

30 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-19 09:31 -0400

1"""Plan file collector.""" 

2 

3from pathlib import Path 

4 

5from workstack.core.context import WorkstackContext 

6from workstack.status.collectors.base import StatusCollector 

7from workstack.status.models.status_data import PlanStatus 

8 

9 

10class PlanFileCollector(StatusCollector): 

11 """Collects information about .PLAN.md file.""" 

12 

13 @property 

14 def name(self) -> str: 

15 """Name identifier for this collector.""" 

16 return "plan" 

17 

18 def is_available(self, ctx: WorkstackContext, worktree_path: Path) -> bool: 

19 """Check if .PLAN.md exists. 

20 

21 Args: 

22 ctx: Workstack context 

23 worktree_path: Path to worktree 

24 

25 Returns: 

26 True if .PLAN.md exists 

27 """ 

28 plan_path = worktree_path / ".PLAN.md" 

29 return plan_path.exists() 

30 

31 def collect( 

32 self, ctx: WorkstackContext, worktree_path: Path, repo_root: Path 

33 ) -> PlanStatus | None: 

34 """Collect plan file information. 

35 

36 Args: 

37 ctx: Workstack context 

38 worktree_path: Path to worktree 

39 repo_root: Repository root path 

40 

41 Returns: 

42 PlanStatus with file information or None if collection fails 

43 """ 

44 plan_path = worktree_path / ".PLAN.md" 

45 

46 if not plan_path.exists(): 

47 return PlanStatus( 

48 exists=False, 

49 path=None, 

50 summary=None, 

51 line_count=0, 

52 first_lines=[], 

53 ) 

54 

55 # Read the file 

56 content = plan_path.read_text(encoding="utf-8") 

57 lines = content.splitlines() 

58 line_count = len(lines) 

59 

60 # Get first 5 lines 

61 first_lines = lines[:5] if len(lines) >= 5 else lines 

62 

63 # Extract summary from first few non-empty lines 

64 summary_lines = [] 

65 for line in lines[:10]: # Look at first 10 lines 

66 stripped = line.strip() 

67 if stripped and not stripped.startswith("#"): 

68 summary_lines.append(stripped) 

69 if len(summary_lines) >= 2: 

70 break 

71 

72 summary = " ".join(summary_lines) if summary_lines else None 

73 

74 # Truncate summary if too long 

75 if summary and len(summary) > 100: 

76 summary = summary[:97] + "..." 

77 

78 return PlanStatus( 

79 exists=True, 

80 path=plan_path, 

81 summary=summary, 

82 line_count=line_count, 

83 first_lines=first_lines, 

84 )