Coverage for src/domain/validation/validation_gating.py: 30%

1"""Validation gating: determines whether validation should trigger based on changed files.

2

3 This module provides logic to filter validation triggers based on patterns

4 from mala.yaml configuration:

5- `code_patterns`: patterns for files that trigger validation

6- `config_files`: patterns for files that trigger validation and invalidate

7 lint/format/typecheck cache

8- `setup_files`: patterns for files that trigger validation and invalidate

9 setup cache (uv.lock, etc.)

10

11 Special cases:

12- Empty `code_patterns` matches all files (validation always triggers)

13- Changes to `mala.yaml` always trigger validation

14- Changes to `config_files` or `setup_files` trigger cache invalidation

15"""

16

17 from __future__ import annotations

18

19 from pathlib import Path

20 from typing import Protocol

21

22 from .code_pattern_matcher import filter_matching_files

23

24

25# Config file name that always triggers validation when changed

26 MALA_CONFIG_FILE = "mala.yaml"

27

28

29 class ValidationSpecLike(Protocol):

30 """Protocol for objects with validation spec attributes.

31

32 This allows duck-typing for testing without requiring the full ValidationSpec.

33 """

34

35 code_patterns: list[str]

36 config_files: list[str]

37 setup_files: list[str]

38

39

40 def should_trigger_validation(

41 changed_files: list[str],

42 spec: ValidationSpecLike,

43) -> bool:

44 """Determine if validation should run based on changed files.

45

46 Validation should trigger when:

47 1. `mala.yaml` is in the changed files (always triggers)

48 2. `code_patterns` is empty (matches all files)

49 3. Any changed file matches a pattern in `code_patterns`, `config_files`,

50 or `setup_files`

51

52 Args:

53 changed_files: List of file paths that were modified.

54 spec: ValidationSpec containing code_patterns to match against.

55

56 Returns:

57 True if validation should run, False to skip validation.

58 """

59 if not changed_files:

60 # No changes = no validation needed

61 return False

62

63 # Special case: mala.yaml changes always trigger validation

64 if any(_is_mala_config(f) for f in changed_files):

65 return True

66

67 # Empty patterns means match all files

68 if not spec.code_patterns:

69 return True

70

71 # Check if any changed file matches code/config/setup patterns

72 patterns = [*spec.code_patterns, *spec.config_files, *spec.setup_files]

73 if not patterns:

74 return True

75 matching_files = filter_matching_files(changed_files, patterns)

76 return len(matching_files) > 0

77

78

79 def get_matching_code_files(

80 changed_files: list[str],

81 spec: ValidationSpecLike,

82) -> list[str]:

83 """Get the subset of changed files that match code patterns.

84

85 This is useful for understanding which files triggered validation.

86

87 Args:

88 changed_files: List of file paths that were modified.

89 spec: ValidationSpec containing code_patterns to match against.

90

91 Returns:

92 List of files that match code patterns (or all files if patterns empty).

93 """

94 if not spec.code_patterns:

95 return list(changed_files)

96 return filter_matching_files(changed_files, spec.code_patterns)

97

98

99 def should_invalidate_lint_cache(

100 changed_files: list[str],

101 spec: ValidationSpecLike,

102) -> bool:

103 """Determine if lint/format/typecheck cache should be invalidated.

104

105 Cache should be invalidated when:

106 - mala.yaml is changed (always)

107 - Any changed file matches `config_files` patterns

108

109 Args:

110 changed_files: List of file paths that were modified.

111 spec: ValidationSpec containing config_files patterns.

112

113 Returns:

114 True if lint cache should be invalidated.

115 """

116 if not changed_files:

117 return False

118

119 # mala.yaml changes always invalidate lint cache

120 if any(_is_mala_config(f) for f in changed_files):

121 return True

122

123 # Check config_files patterns (if any defined)

124 if not spec.config_files:

125 return False

126

127 matching_files = filter_matching_files(changed_files, spec.config_files)

128 return len(matching_files) > 0

129

130

131 def should_invalidate_setup_cache(

132 changed_files: list[str],

133 spec: ValidationSpecLike,

134) -> bool:

135 """Determine if setup cache should be invalidated.

136

137 Setup cache should be invalidated when any changed file matches

138 `setup_files` patterns (e.g., uv.lock, requirements.txt, etc.).

139

140 Args:

141 changed_files: List of file paths that were modified.

142 spec: ValidationSpec containing setup_files patterns.

143

144 Returns:

145 True if setup cache should be invalidated.

146 """

147 if not changed_files or not spec.setup_files:

148 return False

149

150 matching_files = filter_matching_files(changed_files, spec.setup_files)

151 return len(matching_files) > 0

152

153

154 def get_config_files_changed(

155 changed_files: list[str],

156 spec: ValidationSpecLike,

157) -> list[str]:

158 """Get config files that were changed (for logging/debugging).

159

160 Args:

161 changed_files: List of file paths that were modified.

162 spec: ValidationSpec containing config_files patterns.

163

164 Returns:

165 List of changed config files.

166 """

167 if not spec.config_files:

168 return []

169 result = filter_matching_files(changed_files, spec.config_files)

170 # Also include mala.yaml if changed

171 if any(_is_mala_config(f) for f in changed_files):

172 if MALA_CONFIG_FILE not in result:

173 result = [MALA_CONFIG_FILE, *result]

174 return result

175

176

177 def get_setup_files_changed(

178 changed_files: list[str],

179 spec: ValidationSpecLike,

180) -> list[str]:

181 """Get setup files that were changed (for logging/debugging).

182

183 Args:

184 changed_files: List of file paths that were modified.

185 spec: ValidationSpec containing setup_files patterns.

186

187 Returns:

188 List of changed setup files.

189 """

190 if not spec.setup_files:

191 return []

192 return filter_matching_files(changed_files, spec.setup_files)

193

194

195 def _is_mala_config(file_path: str) -> bool:

196 """Check if a file path is the mala config file.

197

198 Handles both absolute and relative paths by checking the basename.

199

200 Args:

201 file_path: File path to check.

202

203 Returns:

204 True if this is the mala.yaml config file.

205 """

206 # Check basename to handle paths like /repo/mala.yaml or mala.yaml

207 return Path(file_path).name == MALA_CONFIG_FILE