claude-dev-env 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +31 -1
  2. package/.agents/skills/e-code-review/SKILL.md +12 -1
  3. package/.agents/skills/e-code-review/reference/fix.md +5 -1
  4. package/.agents/skills/e-code-review/reference/loop.md +4 -0
  5. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
  6. package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
  7. package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
  8. package/.agents/skills/pr-cleanup/SKILL.md +109 -11
  9. package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
  11. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
  12. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
  13. package/docs/CODE_RULES.md +2 -0
  14. package/hooks/advisory/conftest.py +10 -0
  15. package/hooks/advisory/refactor_guard.py +250 -144
  16. package/hooks/advisory/refactor_guard_test_support.py +46 -0
  17. package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
  18. package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
  19. package/hooks/blocking/block_main_commit.py +66 -33
  20. package/hooks/blocking/code_rules_blast_radius.py +194 -0
  21. package/hooks/blocking/code_rules_enforcer.py +95 -0
  22. package/hooks/blocking/codex_apply_patch.py +238 -0
  23. package/hooks/blocking/test_block_main_commit.py +145 -0
  24. package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
  25. package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +148 -0
  26. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +1 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +154 -138
  28. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +52 -9
  29. package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
  30. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +71 -5
  31. package/hooks/git-hooks/AGENTS.md +1 -1
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
  33. package/hooks/git-hooks/post_commit.py +160 -51
  34. package/hooks/git-hooks/pre_commit.py +3 -3
  35. package/hooks/git-hooks/test_post_commit.py +203 -0
  36. package/hooks/git-hooks/test_pre_commit.py +2 -2
  37. package/hooks/hooks_constants/blast_radius_constants.py +14 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +1 -0
  39. package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
  40. package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
  41. package/hooks/observability/test_instructions_loaded_logger.py +54 -0
  42. package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
  43. package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
  44. package/hooks/validation/mypy_validator.py +213 -80
  45. package/hooks/validation/test_mypy_validator.py +288 -13
  46. package/hooks/workflow/auto_formatter.py +225 -93
  47. package/hooks/workflow/investigation_tracker_reset.py +2 -0
  48. package/hooks/workflow/test_auto_formatter.py +261 -12
  49. package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
  50. package/package.json +1 -1
  51. package/rules/failure-blast-radius.md +126 -0
  52. package/scripts/codex_compat_materializer.py +395 -17
  53. package/scripts/tests/test_codex_compat_materializer.py +17 -3
@@ -11,11 +11,13 @@ This catches:
11
11
  Works in both WSL and Windows for any Python project with a git root.
12
12
  Project root is discovered via CLAUDE_PROJECT_ROOT env var or git rev-parse.
13
13
  """
14
+
14
15
  import hashlib
15
16
  import importlib
16
17
  import json
17
18
  import os
18
19
  import platform
20
+ import stat
19
21
  import subprocess
20
22
  import sys
21
23
  from pathlib import Path
@@ -33,9 +35,17 @@ if _validators_directory not in sys.path:
33
35
  if _hooks_directory not in sys.path:
34
36
  sys.path.insert(0, _hooks_directory)
35
37
 
36
- from mypy_integration import find_pyproject_with_mypy_config # noqa: E402
38
+ from mypy_integration import ( # noqa: E402
39
+ ancestor_directories,
40
+ find_pyproject_with_mypy_config,
41
+ )
37
42
 
43
+ from atomic_file_writer import write_text_atomically # noqa: E402
44
+ from hooks_constants.atomic_file_writer_constants import ( # noqa: E402
45
+ ATOMIC_WRITE_TEMPORARY_SUFFIX,
46
+ )
38
47
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
48
+ from hooks_constants.mypy_integration_constants import PYPROJECT_FILENAME # noqa: E402
39
49
  from hooks_constants.mypy_validator_cache_constants import ( # noqa: E402
40
50
  CACHE_FILE_ENCODING,
41
51
  CONTENT_HASH_CACHE_PASSING_EXIT_CODE,
@@ -45,6 +55,7 @@ from hooks_constants.mypy_validator_cache_constants import ( # noqa: E402
45
55
  SESSION_ID_ENVIRONMENT_VARIABLE,
46
56
  UNKNOWN_SESSION_IDENTIFIER,
47
57
  )
58
+ from json_file_reader import read_json_object # noqa: E402
48
59
 
49
60
 
50
61
  def load_notification_utils() -> ModuleType | None:
@@ -62,6 +73,8 @@ MAXIMUM_DISPLAYED_ERRORS = 5
62
73
 
63
74
  SKIP_PATTERNS = {"test_", "_test.", "conftest", "/tests/", "\\tests\\", "fixture", "mock"}
64
75
 
76
+ ConfigCacheEntry = tuple[str, str | None, tuple[str, ...], str | None]
77
+
65
78
 
66
79
  def discover_project_root(target_file: str) -> Path | None:
67
80
  if env_root := os.environ.get("CLAUDE_PROJECT_ROOT"):
@@ -70,7 +83,8 @@ def discover_project_root(target_file: str) -> Path | None:
70
83
  try:
71
84
  completed_process = subprocess.run(
72
85
  ["git", "rev-parse", "--show-toplevel"],
73
- check=False, capture_output=True,
86
+ check=False,
87
+ capture_output=True,
74
88
  text=True,
75
89
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
76
90
  cwd=str(Path(target_file).parent),
@@ -90,7 +104,7 @@ def is_file_within_project(target_file: str, project_root: Path) -> bool:
90
104
  return False
91
105
 
92
106
 
93
- _session_config_cache_by_target_directory: dict[str, str | None] = {}
107
+ _session_config_cache_by_target_directory: dict[str, ConfigCacheEntry] = {}
94
108
 
95
109
 
96
110
  def reset_session_config_cache() -> None:
@@ -119,35 +133,96 @@ def _session_cache_path(cache_filename: str) -> Path:
119
133
  return Path(HOOK_STATE_CACHE_DIRECTORY) / session_identifier / cache_filename
120
134
 
121
135
 
122
- def _read_cache_file(cache_path: Path) -> dict[str, object]:
123
- if not cache_path.is_file():
124
- return {}
125
- try:
126
- raw_text = cache_path.read_text(encoding=CACHE_FILE_ENCODING)
127
- except OSError:
128
- return {}
129
- if not raw_text.strip():
130
- return {}
131
- try:
132
- parsed_cache = json.loads(raw_text)
133
- except json.JSONDecodeError:
134
- return {}
135
- return parsed_cache if isinstance(parsed_cache, dict) else {}
136
+ def _walk_mypy_config(target_file: Path) -> Path | None:
137
+ discovered_config = find_pyproject_with_mypy_config(target_file)
138
+ return discovered_config if isinstance(discovered_config, Path) else None
136
139
 
137
140
 
138
- def _write_cache_file(cache_path: Path, cache_by_key: dict[str, object]) -> None:
139
- try:
140
- cache_path.parent.mkdir(parents=True, exist_ok=True)
141
- cache_path.write_text(
142
- json.dumps(cache_by_key), encoding=CACHE_FILE_ENCODING
143
- )
144
- except OSError:
145
- return
141
+ def _config_candidate_paths(target_file: Path) -> tuple[str, ...]:
142
+ return tuple(
143
+ str(each_directory / PYPROJECT_FILENAME)
144
+ for each_directory in ancestor_directories(target_file)
145
+ )
146
146
 
147
147
 
148
- def _walk_mypy_config(target_file: Path) -> Path | None:
149
- discovered_config = find_pyproject_with_mypy_config(target_file)
150
- return discovered_config if isinstance(discovered_config, Path) else None
148
+ def _configuration_metadata_signature(all_candidate_paths: tuple[str, ...]) -> str:
149
+ config_search_hasher = hashlib.sha256()
150
+ for each_candidate_path in all_candidate_paths:
151
+ config_search_hasher.update(each_candidate_path.encode(CACHE_FILE_ENCODING))
152
+ try:
153
+ candidate_stat = Path(each_candidate_path).stat()
154
+ except OSError:
155
+ config_search_hasher.update(b"missing")
156
+ continue
157
+ if not stat.S_ISREG(candidate_stat.st_mode):
158
+ config_search_hasher.update(b"non-file")
159
+ continue
160
+ config_search_hasher.update(b"file")
161
+ for each_metadata_field in (
162
+ candidate_stat.st_ino,
163
+ candidate_stat.st_size,
164
+ candidate_stat.st_mtime_ns,
165
+ candidate_stat.st_ctime_ns,
166
+ ):
167
+ config_search_hasher.update(str(each_metadata_field).encode(CACHE_FILE_ENCODING))
168
+ config_search_hasher.update(b";")
169
+ return config_search_hasher.hexdigest()
170
+
171
+
172
+ def _configuration_content_digest(
173
+ config_path: str | None, all_candidate_paths: tuple[str, ...]
174
+ ) -> str | None:
175
+ paths_to_fingerprint = (config_path,) if config_path is not None else all_candidate_paths
176
+ content_hasher = hashlib.sha256()
177
+ for each_config_path in paths_to_fingerprint:
178
+ content_hasher.update(each_config_path.encode(CACHE_FILE_ENCODING))
179
+ try:
180
+ content_hasher.update(Path(each_config_path).read_bytes())
181
+ except OSError:
182
+ content_hasher.update(b"unreadable")
183
+ return content_hasher.hexdigest()
184
+
185
+
186
+ def _read_cached_config_entry(cached_entry: object) -> ConfigCacheEntry | None:
187
+ if not isinstance(cached_entry, dict):
188
+ return None
189
+ cached_signature = cached_entry.get("signature")
190
+ cached_config_path = cached_entry.get("config_path")
191
+ raw_candidate_paths = cached_entry.get("candidate_paths")
192
+ cached_content_digest = cached_entry.get("content_digest")
193
+ if not isinstance(cached_signature, str):
194
+ return None
195
+ if cached_config_path is not None and not isinstance(cached_config_path, str):
196
+ return None
197
+ if cached_content_digest is not None and not isinstance(cached_content_digest, str):
198
+ return None
199
+ if not isinstance(raw_candidate_paths, list):
200
+ return None
201
+ all_candidate_paths: list[str] = []
202
+ for each_candidate_path in raw_candidate_paths:
203
+ if not isinstance(each_candidate_path, str):
204
+ return None
205
+ all_candidate_paths.append(each_candidate_path)
206
+ if not all_candidate_paths:
207
+ return None
208
+ if cached_config_path is not None and cached_config_path not in all_candidate_paths:
209
+ return None
210
+ return (
211
+ cached_signature,
212
+ cached_config_path,
213
+ tuple(all_candidate_paths),
214
+ cached_content_digest,
215
+ )
216
+
217
+
218
+ def _serialize_config_cache_entry(cache_entry: ConfigCacheEntry) -> dict[str, object]:
219
+ config_signature, config_path, all_candidate_paths, content_digest = cache_entry
220
+ return {
221
+ "signature": config_signature,
222
+ "config_path": config_path,
223
+ "candidate_paths": list(all_candidate_paths),
224
+ "content_digest": content_digest,
225
+ }
151
226
 
152
227
 
153
228
  def _config_cache_key_for(target_file: Path) -> str:
@@ -189,49 +264,88 @@ def discover_mypy_config(target_file: Path) -> Path | None:
189
264
  table, or None when none exists above the file.
190
265
  """
191
266
  cache_key = _config_cache_key_for(target_file)
192
- if cache_key in _session_config_cache_by_target_directory:
193
- cached_value = _session_config_cache_by_target_directory[cache_key]
194
- return Path(cached_value) if cached_value is not None else None
195
-
196
- config_cache_path = _session_cache_path(MYPY_CONFIG_CACHE_FILENAME)
197
- persisted_cache = _read_cache_file(config_cache_path)
198
- if cache_key in persisted_cache:
199
- persisted_value = persisted_cache[cache_key]
200
- resolved_persisted = persisted_value if isinstance(persisted_value, str) else None
201
- _session_config_cache_by_target_directory[cache_key] = resolved_persisted
202
- return Path(resolved_persisted) if resolved_persisted is not None else None
267
+ cached_entry = _session_config_cache_by_target_directory.get(cache_key)
268
+ if cached_entry is not None:
269
+ config_search_signature = _configuration_metadata_signature(cached_entry[2])
270
+ current_content_digest = _configuration_content_digest(cached_entry[1], cached_entry[2])
271
+ is_content_unchanged = current_content_digest == cached_entry[3]
272
+ if config_search_signature == cached_entry[0] and is_content_unchanged:
273
+ cached_config_path = cached_entry[1]
274
+ return Path(cached_config_path) if cached_config_path is not None else None
275
+ all_candidate_paths = cached_entry[2]
276
+ config_cache_path = _session_cache_path(MYPY_CONFIG_CACHE_FILENAME)
277
+ persisted_cache = read_json_object(config_cache_path, encoding=CACHE_FILE_ENCODING) or {}
278
+ else:
279
+ config_cache_path = _session_cache_path(MYPY_CONFIG_CACHE_FILENAME)
280
+ persisted_cache = read_json_object(config_cache_path, encoding=CACHE_FILE_ENCODING) or {}
281
+ persisted_entry = _read_cached_config_entry(persisted_cache.get(cache_key))
282
+ if persisted_entry is not None:
283
+ config_search_signature = _configuration_metadata_signature(persisted_entry[2])
284
+ current_content_digest = _configuration_content_digest(
285
+ persisted_entry[1], persisted_entry[2]
286
+ )
287
+ is_content_unchanged = current_content_digest == persisted_entry[3]
288
+ if config_search_signature == persisted_entry[0] and is_content_unchanged:
289
+ _session_config_cache_by_target_directory[cache_key] = persisted_entry
290
+ cached_config_path = persisted_entry[1]
291
+ return Path(cached_config_path) if cached_config_path is not None else None
292
+ all_candidate_paths = persisted_entry[2]
293
+ else:
294
+ all_candidate_paths = _config_candidate_paths(target_file)
295
+ config_search_signature = _configuration_metadata_signature(all_candidate_paths)
203
296
 
204
297
  discovered_config = _walk_mypy_config(target_file)
205
298
  discovered_value = str(discovered_config) if discovered_config is not None else None
206
- _session_config_cache_by_target_directory[cache_key] = discovered_value
207
- persisted_cache[cache_key] = discovered_value
208
- _write_cache_file(config_cache_path, persisted_cache)
299
+ content_digest = _configuration_content_digest(discovered_value, all_candidate_paths)
300
+ refreshed_entry = (
301
+ config_search_signature,
302
+ discovered_value,
303
+ all_candidate_paths,
304
+ content_digest,
305
+ )
306
+ _session_config_cache_by_target_directory[cache_key] = refreshed_entry
307
+ persisted_cache[cache_key] = _serialize_config_cache_entry(refreshed_entry)
308
+ try:
309
+ write_text_atomically(
310
+ config_cache_path,
311
+ json.dumps(persisted_cache),
312
+ encoding=CACHE_FILE_ENCODING,
313
+ temporary_prefix=f".{config_cache_path.name}-",
314
+ temporary_suffix=ATOMIC_WRITE_TEMPORARY_SUFFIX,
315
+ should_reap_orphans=True,
316
+ )
317
+ except OSError:
318
+ pass
209
319
  return discovered_config
210
320
 
211
321
 
212
322
  def _config_signature(mypy_config_file: Path | None) -> bytes:
213
323
  """Return a byte signature of the discovered mypy config's current contents.
214
324
 
215
- The signature folds the config file's own bytes into the content-hash cache
216
- key so a change to the project's ``[tool.mypy]`` settings invalidates a
217
- previously recorded passing hash: when the file's bytes are restored to a
218
- prior passing version under a tightened config, the composite hash differs
219
- and mypy re-runs rather than returning a stale pass. An absent config
325
+ The signature folds the resolved config path and the config file's bytes
326
+ into the content-hash cache key so a configuration relocation or change
327
+ invalidates a previously recorded passing hash. An absent config
220
328
  contributes a fixed empty signature.
221
329
 
222
330
  Args:
223
331
  mypy_config_file: The discovered config path, or None when none exists.
224
332
 
225
333
  Returns:
226
- The config file's bytes, or an empty signature when there is no config
227
- or it cannot be read.
334
+ The resolved config path and bytes, or the path alone when the config
335
+ cannot be read.
228
336
  """
229
337
  if mypy_config_file is None:
230
338
  return b""
231
339
  try:
232
- return mypy_config_file.read_bytes()
340
+ resolved_config_path = str(mypy_config_file.resolve())
341
+ except (OSError, RuntimeError):
342
+ resolved_config_path = str(mypy_config_file)
343
+ resolved_config_path_bytes = resolved_config_path.encode(CACHE_FILE_ENCODING)
344
+ try:
345
+ config_bytes = mypy_config_file.read_bytes()
233
346
  except OSError:
234
- return b""
347
+ return resolved_config_path_bytes
348
+ return resolved_config_path_bytes + b"\0" + config_bytes
235
349
 
236
350
 
237
351
  def _composite_content_hash(target_file: str, mypy_config_file: Path | None) -> str | None:
@@ -255,8 +369,12 @@ def _composite_content_hash(target_file: str, mypy_config_file: Path | None) ->
255
369
 
256
370
 
257
371
  def _read_cached_passing_hash(target_file: str) -> str | None:
258
- content_hash_cache = _read_cache_file(
259
- _session_cache_path(MYPY_CONTENT_HASH_CACHE_FILENAME)
372
+ content_hash_cache = (
373
+ read_json_object(
374
+ _session_cache_path(MYPY_CONTENT_HASH_CACHE_FILENAME),
375
+ encoding=CACHE_FILE_ENCODING,
376
+ )
377
+ or {}
260
378
  )
261
379
  cached_hash = content_hash_cache.get(target_file)
262
380
  return cached_hash if isinstance(cached_hash, str) else None
@@ -264,9 +382,21 @@ def _read_cached_passing_hash(target_file: str) -> str | None:
264
382
 
265
383
  def _record_passing_hash(target_file: str, content_hash: str) -> None:
266
384
  content_hash_cache_path = _session_cache_path(MYPY_CONTENT_HASH_CACHE_FILENAME)
267
- content_hash_cache = _read_cache_file(content_hash_cache_path)
385
+ content_hash_cache = (
386
+ read_json_object(content_hash_cache_path, encoding=CACHE_FILE_ENCODING) or {}
387
+ )
268
388
  content_hash_cache[target_file] = content_hash
269
- _write_cache_file(content_hash_cache_path, content_hash_cache)
389
+ try:
390
+ write_text_atomically(
391
+ content_hash_cache_path,
392
+ json.dumps(content_hash_cache),
393
+ encoding=CACHE_FILE_ENCODING,
394
+ temporary_prefix=f".{content_hash_cache_path.name}-",
395
+ temporary_suffix=ATOMIC_WRITE_TEMPORARY_SUFFIX,
396
+ should_reap_orphans=True,
397
+ )
398
+ except OSError:
399
+ pass
270
400
 
271
401
 
272
402
  def build_mypy_command(relative_file_path: str, mypy_config_file: Path | None) -> list[str]:
@@ -286,12 +416,16 @@ def build_mypy_command(relative_file_path: str, mypy_config_file: Path | None) -
286
416
  config_arguments = (
287
417
  ["--config-file", str(mypy_config_file)] if mypy_config_file is not None else []
288
418
  )
289
- return base_command + config_arguments + [
290
- "--no-error-summary",
291
- "--show-error-codes",
292
- "--no-color",
293
- relative_file_path,
294
- ]
419
+ return (
420
+ base_command
421
+ + config_arguments
422
+ + [
423
+ "--no-error-summary",
424
+ "--show-error-codes",
425
+ "--no-color",
426
+ relative_file_path,
427
+ ]
428
+ )
295
429
 
296
430
 
297
431
  def project_relative_path(target_file: str, project_root: str) -> str:
@@ -319,20 +453,14 @@ def project_relative_path(target_file: str, project_root: str) -> str:
319
453
  def run_mypy(target_file: str, project_root: str) -> tuple[int, str]:
320
454
  """Run mypy on one file from the project root and return its result.
321
455
 
322
- The mypy run is skipped when a composite hash over the target file's bytes
323
- and its discovered mypy config's bytes matches the hash recorded the last
324
- time mypy passed for that file; that recorded skip can only return a pass, so
325
- a content change always re-runs mypy and a file edited to introduce a type
326
- error still blocks. Folding the config bytes into the hash invalidates the
327
- skip when the project's ``[tool.mypy]`` settings change, so a file whose
328
- bytes are restored to a prior passing version under a tightened config
329
- re-runs rather than returning a stale pass. The discovered config is reused
330
- from the per-session cache keyed by the target file's own directory, so two
331
- files in sibling subtrees under one project root each resolve their own
332
- nearer config.
333
-
334
- The composite hash covers the target file's own bytes and its config's
335
- bytes only, so the skip is blind to a cross-file change in a dependency:
456
+ The mypy run is skipped when a composite hash over the target file's bytes,
457
+ resolved config path, and config bytes matches the hash recorded the last
458
+ time mypy passed for that file. A source or configuration change then
459
+ re-runs mypy, while the discovered config is reused from metadata cached by
460
+ the target file's own directory.
461
+
462
+ The composite hash covers the target file's own bytes, resolved config path,
463
+ and config bytes, so the skip is blind to a cross-file change in a dependency:
336
464
  when a dependency is edited in a way that breaks this file's call site and
337
465
  this file is later rewritten to its prior passing content, the cached pass
338
466
  returns without re-running mypy. The post-write hook already type-checks only
@@ -359,7 +487,8 @@ def run_mypy(target_file: str, project_root: str) -> tuple[int, str]:
359
487
 
360
488
  completed_process = subprocess.run(
361
489
  mypy_command,
362
- check=False, capture_output=True,
490
+ check=False,
491
+ capture_output=True,
363
492
  text=True,
364
493
  env=os.environ.copy(),
365
494
  timeout=MYPY_TIMEOUT_SECONDS,
@@ -368,9 +497,14 @@ def run_mypy(target_file: str, project_root: str) -> tuple[int, str]:
368
497
 
369
498
  stdout_output = completed_process.stdout.strip()
370
499
  stderr_output = completed_process.stderr.strip()
371
- combined_output = f"{stdout_output}\n{stderr_output}".strip() if stderr_output else stdout_output
500
+ combined_output = (
501
+ f"{stdout_output}\n{stderr_output}".strip() if stderr_output else stdout_output
502
+ )
372
503
 
373
- if completed_process.returncode == CONTENT_HASH_CACHE_PASSING_EXIT_CODE and content_hash is not None:
504
+ if (
505
+ completed_process.returncode == CONTENT_HASH_CACHE_PASSING_EXIT_CODE
506
+ and content_hash is not None
507
+ ):
374
508
  _record_passing_hash(target_file, content_hash)
375
509
 
376
510
  return completed_process.returncode, combined_output
@@ -435,8 +569,7 @@ def is_test_file(python_file: Path) -> bool:
435
569
  path_lower = str(python_file).lower()
436
570
 
437
571
  return any(
438
- each_pattern in name_lower or each_pattern in path_lower
439
- for each_pattern in SKIP_PATTERNS
572
+ each_pattern in name_lower or each_pattern in path_lower for each_pattern in SKIP_PATTERNS
440
573
  )
441
574
 
442
575