Coverage for little_loops/work_verification.py: 15%

1"""Work verification utilities for little-loops.

2

3 Contains shared functions for verifying that actual implementation work

4 was done, used by both issue_manager (ll-auto) and worker_pool (ll-parallel).

5"""

6

7 from __future__ import annotations

8

9 import subprocess

10 from typing import TYPE_CHECKING

11

12 if TYPE_CHECKING:

13 from little_loops.logger import Logger

14

15

16# Directories that are excluded when verifying work was done.

17# Changes to files in these directories don't count as "real work".

18 EXCLUDED_DIRECTORIES = (

19 ".issues/",

20 "issues/", # Support non-dotted variant (issues.base_dir = "issues")

21 ".speckit/",

22 "thoughts/",

23 ".worktrees/",

24 ".auto-manage",

25)

26

27

28 def filter_excluded_files(files: list[str]) -> list[str]:

29 """Filter out files in excluded directories.

30

31 Args:

32 files: List of file paths to filter

33

34 Returns:

35 List of files not in excluded directories

36 """

37 return [

38 f

39 for f in files

40 if f and not any(f.startswith(excluded) for excluded in EXCLUDED_DIRECTORIES)

41 ]

42

43

44 def verify_work_was_done(logger: Logger, changed_files: list[str] | None = None) -> bool:

45 """Verify that actual work was done (not just issue file moves).

46

47 Returns True if there's evidence of implementation work - changes to files

48 outside of excluded directories like .issues/, thoughts/, etc.

49

50 This prevents marking issues as "completed" when no actual fix was implemented.

51

52 Args:

53 logger: Logger for output

54 changed_files: Optional list of changed files. If not provided,

55 will detect via git diff commands.

56

57 Returns:

58 True if meaningful file changes were detected

59 """

60 # If changed_files provided, use them directly (ll-parallel case)

61 if changed_files is not None:

62 meaningful_changes = filter_excluded_files(changed_files)

63 if meaningful_changes:

64 logger.info(

65 f"Found {len(meaningful_changes)} file(s) changed: {meaningful_changes[:5]}"

66 )

67 return True

68 # Log which excluded files were modified for diagnostic purposes

69 excluded_files = [f for f in changed_files if f]

70 logger.warning(

71 f"No meaningful changes detected - only excluded files modified: {excluded_files[:10]}"

72 )

73 return False

74

75 # Otherwise detect via git (ll-auto case)

76 all_excluded_files: list[str] = []

77 try:

78 # Check for uncommitted changes

79 result = subprocess.run(

80 ["git", "diff", "--name-only"],

81 capture_output=True,

82 text=True,

83 )

84 if result.returncode == 0:

85 files = result.stdout.strip().split("\n")

86 meaningful_changes = filter_excluded_files(files)

87 if meaningful_changes:

88 logger.info(

89 f"Found {len(meaningful_changes)} file(s) changed: {meaningful_changes[:5]}"

90 )

91 return True

92 # Collect excluded files for diagnostic logging

93 all_excluded_files.extend([f for f in files if f])

94

95 # Also check staged changes

96 result = subprocess.run(

97 ["git", "diff", "--cached", "--name-only"],

98 capture_output=True,

99 text=True,

100 )

101 if result.returncode == 0:

102 staged = result.stdout.strip().split("\n")

103 meaningful_staged = filter_excluded_files(staged)

104 if meaningful_staged:

105 logger.info(

106 f"Found {len(meaningful_staged)} staged file(s): {meaningful_staged[:5]}"

107 )

108 return True

109 # Collect excluded files for diagnostic logging

110 all_excluded_files.extend([f for f in staged if f and f not in all_excluded_files])

111

112 # Log which excluded files were modified for diagnostic purposes

113 if all_excluded_files:

114 logger.warning(

115 f"No meaningful changes detected - only excluded files modified: "

116 f"{all_excluded_files[:10]}"

117 )

118 else:

119 logger.warning("No meaningful changes detected - no files modified")

120 return False

121

122 except Exception as e:

123 logger.error(f"Could not verify work: {e}")

124 # Be conservative - don't assume work was done if we can't verify

125 return False