claude-dev-env 1.78.0 → 1.80.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 (63) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  5. package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
  6. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  7. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  8. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  9. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
  10. package/bin/install.mjs +1 -0
  11. package/bin/install.test.mjs +3 -2
  12. package/hooks/blocking/CLAUDE.md +3 -2
  13. package/hooks/blocking/code_rules_docstrings.py +609 -16
  14. package/hooks/blocking/code_rules_enforcer.py +41 -0
  15. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  16. package/hooks/blocking/code_rules_naming_collection.py +76 -1
  17. package/hooks/blocking/code_rules_paired_test.py +240 -22
  18. package/hooks/blocking/code_rules_test_assertions.py +159 -1
  19. package/hooks/blocking/code_rules_unused_imports.py +7 -63
  20. package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
  21. package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
  22. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
  23. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
  24. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
  25. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
  26. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  27. package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
  28. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
  29. package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
  31. package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
  32. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  33. package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
  34. package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
  36. package/hooks/hooks_constants/CLAUDE.md +1 -0
  37. package/hooks/hooks_constants/blocking_check_limits.py +79 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
  39. package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
  40. package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
  41. package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  43. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  44. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/CLAUDE.md +1 -0
  47. package/rules/docstring-prose-matches-implementation.md +57 -54
  48. package/rules/env-var-table-code-drift.md +24 -0
  49. package/rules/package-inventory-stale-entry.md +4 -4
  50. package/rules/paired-test-coverage.md +12 -5
  51. package/skills/autoconverge/SKILL.md +26 -5
  52. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  53. package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
  54. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
  55. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  56. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  57. package/skills/autoconverge/workflow/converge.mjs +110 -228
  58. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  59. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  60. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  61. package/skills/pr-converge/SKILL.md +28 -2
  62. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  63. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -3,29 +3,48 @@
3
3
  The TDD gate ``tdd_enforcer.py`` requires a fresh test file to exist before a
4
4
  production module is written, but it judges coverage at file granularity: a
5
5
  module whose dedicated test file exercises some public functions while leaving
6
- one untested still satisfies it. This check closes that gap. When a production
7
- module has an established paired test suite a stem-matched ``test_<stem>.py``
8
- that already exercises at least one of the module's public functions — a public
9
- function that suite references nowhere is flagged, so the forgotten function
10
- gets a behavioral test before the partially-covered module lands.
11
-
12
- The check stays conservative to keep false positives near zero:
13
-
14
- - It runs only on a production module whose stem-matched test file already
15
- exists; a module with no dedicated test file is out of scope and left to the
16
- file-level TDD gate.
17
- - It fires only when the suite already covers at least one public function of
18
- the same module the signature of a maintained per-function suite rather than
19
- an unrelated or placeholder test file.
20
- - A public function counts as covered when its name appears in any test file
21
- beside the stem-matched one imported, called, or named so a function
22
- exercised by a differently-named sibling test still counts.
6
+ one untested still satisfies it. This check closes that gap from both write
7
+ orders, so a forgotten function gets a behavioral test before the
8
+ partially-covered module lands.
9
+
10
+ ``check_public_function_missing_paired_test`` fires on the production-module
11
+ write: when a module has an established paired test suite — a stem-matched
12
+ ``test_<stem>.py`` that already exercises the module, either by covering at least
13
+ one of its public functions or by referencing one of its private helpers — a
14
+ public function that suite references nowhere is flagged.
15
+
16
+ ``check_test_file_omits_module_public_function`` fires on the stem-matched
17
+ test-file write, covering the reverse order in which the production module is
18
+ written before its test file exists. It resolves the production module the
19
+ written ``test_<stem>.py`` or ``<stem>_test.py`` file pairs with and flags every
20
+ public function the post-edit suite omits, so the gap that the production-side
21
+ check cannot seebecause no suite existed when the module was written — is
22
+ caught when the test file lands.
23
+
24
+ Both checks stay conservative to keep false positives near zero:
25
+
26
+ - The production-side check runs only on a production module whose stem-matched
27
+ test file already exists; a module with no dedicated test file is out of scope
28
+ and left to the file-level TDD gate. The test-side check runs only on a
29
+ stem-matched test file whose paired production module exists on disk.
30
+ - The production-side check fires only when the suite already exercises the
31
+ module — covering at least one of its public functions, or referencing one of
32
+ its private helpers by name — the signature of a maintained per-module suite
33
+ rather than an unrelated or placeholder test file. A suite that exercises only
34
+ a private helper while omitting the public surface is the exact shape the
35
+ private-helper establishment catches. The test-side check fires only when the
36
+ post-edit suite already covers at least one public function of the paired
37
+ module.
38
+ - A public function counts as covered when its name appears in any test file in
39
+ the suite directory — imported, called, or named — so a function exercised by
40
+ a differently-named sibling test still counts.
23
41
  - ``main`` and underscore-prefixed functions are never required to carry a test.
24
42
  - Test modules, hook infrastructure, config modules, migrations, workflow
25
- registries, and ``__init__`` modules are exempt.
43
+ registries, and ``__init__`` modules are exempt as production targets.
26
44
  """
27
45
 
28
46
  import ast
47
+ import os
29
48
  import sys
30
49
  from pathlib import Path
31
50
 
@@ -59,6 +78,7 @@ from hooks_constants.paired_test_coverage_constants import ( # noqa: E402
59
78
  PYTHON_SOURCE_SUFFIX,
60
79
  STEM_TEST_FILENAME_PREFIX,
61
80
  STEM_TEST_FILENAME_SUFFIX,
81
+ TEST_SUITE_OMITS_FUNCTION_GUIDANCE,
62
82
  TESTS_DIRECTORY_NAME,
63
83
  )
64
84
 
@@ -97,6 +117,26 @@ def _public_function_definitions(tree: ast.Module) -> list[tuple[str, int]]:
97
117
  return all_definitions
98
118
 
99
119
 
120
+ def _private_function_names(tree: ast.Module) -> set[str]:
121
+ """Return the names of module-scope underscore-prefixed function definitions.
122
+
123
+ Args:
124
+ tree: The parsed production module.
125
+
126
+ Returns:
127
+ Each module-scope function name that begins with an underscore - the
128
+ private helpers a paired test suite reaches when it exercises the
129
+ module's internals.
130
+ """
131
+ all_private_names: set[str] = set()
132
+ for each_statement in tree.body:
133
+ if not isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
134
+ continue
135
+ if each_statement.name.startswith("_"):
136
+ all_private_names.add(each_statement.name)
137
+ return all_private_names
138
+
139
+
100
140
  def _ancestor_tests_directories(start_directory: Path) -> list[Path]:
101
141
  """Return each ancestor's ``tests`` directory, nearest ancestor first.
102
142
 
@@ -235,7 +275,8 @@ def check_public_function_missing_paired_test(
235
275
  """Flag a public function the module's established paired test suite omits.
236
276
 
237
277
  Runs only on a production module whose stem-matched ``test_<stem>.py`` already
238
- exists and already exercises at least one of the module's public functions.
278
+ exists and already exercises the module — by covering at least one of its
279
+ public functions, or by referencing one of its private helpers by name.
239
280
  Under that established-suite precondition, a public function the suite
240
281
  references nowhere is flagged, so the forgotten function gets a behavioral
241
282
  test before the partially-covered module lands. ``main`` and
@@ -255,8 +296,9 @@ def check_public_function_missing_paired_test(
255
296
  Returns:
256
297
  One violation per uncovered public function, capped at the configured
257
298
  maximum, scoped to the changed lines unless deferred or unscoped. Empty
258
- when the module is exempt, has no dedicated test file, the suite covers
259
- no public function, or the content fails to parse.
299
+ when the module is exempt, has no dedicated test file, the suite neither
300
+ covers a public function nor references a private helper of the module,
301
+ or the content fails to parse.
260
302
  """
261
303
  if _module_is_exempt(file_path):
262
304
  return []
@@ -276,7 +318,14 @@ def check_public_function_missing_paired_test(
276
318
  for each_name, _each_line in all_public_definitions
277
319
  if each_name in all_referenced_identifiers
278
320
  )
279
- if covered_function_count < MINIMUM_COVERED_PUBLIC_FUNCTIONS:
321
+ suite_references_private_helper = bool(
322
+ _private_function_names(tree) & all_referenced_identifiers
323
+ )
324
+ suite_is_established = (
325
+ covered_function_count >= MINIMUM_COVERED_PUBLIC_FUNCTIONS
326
+ or suite_references_private_helper
327
+ )
328
+ if not suite_is_established:
280
329
  return []
281
330
  all_violations_in_walk_order: list[tuple[range, str]] = []
282
331
  for each_name, each_definition_line in all_public_definitions:
@@ -297,3 +346,172 @@ def check_public_function_missing_paired_test(
297
346
  defer_scope_to_caller,
298
347
  )
299
348
 
349
+
350
+ def _production_stem_from_test_filename(test_filename: str) -> str | None:
351
+ """Return the production-module stem a stem-matched test filename pairs with.
352
+
353
+ Args:
354
+ test_filename: The basename of the written file.
355
+
356
+ Returns:
357
+ The production stem for a ``test_<stem>.py`` or ``<stem>_test.py`` name,
358
+ or None when the filename is not stem-matched or its stem is empty.
359
+ """
360
+ if test_filename.endswith(STEM_TEST_FILENAME_SUFFIX):
361
+ production_stem = test_filename[: -len(STEM_TEST_FILENAME_SUFFIX)]
362
+ return production_stem or None
363
+ if test_filename.startswith(STEM_TEST_FILENAME_PREFIX) and test_filename.endswith(
364
+ PYTHON_SOURCE_SUFFIX
365
+ ):
366
+ production_stem = test_filename[
367
+ len(STEM_TEST_FILENAME_PREFIX) : -len(PYTHON_SOURCE_SUFFIX)
368
+ ]
369
+ return production_stem or None
370
+ return None
371
+
372
+
373
+ def _paired_production_module_path(
374
+ test_path: Path, production_stem: str
375
+ ) -> Path | None:
376
+ """Return the production module a stem-matched test file pairs with, or None.
377
+
378
+ Mirrors ``_stem_matched_test_path`` in reverse for the two common layouts: a
379
+ module beside the test file, and a module in the parent of the ``tests``
380
+ directory that directly holds the test file.
381
+
382
+ Args:
383
+ test_path: The resolved path of the written stem-matched test file.
384
+ production_stem: The production-module stem the test filename names.
385
+
386
+ Returns:
387
+ The first candidate production module path that exists on disk, or None
388
+ when no candidate module file exists.
389
+ """
390
+ production_filename = production_stem + PYTHON_SOURCE_SUFFIX
391
+ test_directory = test_path.parent
392
+ all_candidate_paths = [test_directory / production_filename]
393
+ if test_directory.name == TESTS_DIRECTORY_NAME:
394
+ all_candidate_paths.append(test_directory.parent / production_filename)
395
+ for each_candidate_path in all_candidate_paths:
396
+ if each_candidate_path.is_file():
397
+ return each_candidate_path
398
+ return None
399
+
400
+
401
+ def _post_edit_suite_referenced_identifiers(
402
+ suite_directory: Path,
403
+ written_test_path: Path,
404
+ written_test_content: str,
405
+ ) -> set[str]:
406
+ """Return suite identifiers, reading the written test's post-edit content.
407
+
408
+ Scans every ``test_*.py`` and ``*_test.py`` file in the suite directory,
409
+ bounded by the scan cap, and unions the identifiers each references. The
410
+ written test file contributes ``written_test_content`` rather than its stale
411
+ on-disk text, so the post-edit suite is judged.
412
+
413
+ Args:
414
+ suite_directory: The directory holding the suite's test files.
415
+ written_test_path: The resolved path of the test file being written.
416
+ written_test_content: The post-edit text of the written test file.
417
+
418
+ Returns:
419
+ The union of referenced identifiers across the post-edit suite.
420
+ """
421
+ all_test_file_paths: list[Path] = []
422
+ for each_glob in ALL_TEST_FILENAME_GLOBS:
423
+ all_test_file_paths.extend(sorted(suite_directory.glob(each_glob)))
424
+ written_path_key = os.path.normcase(str(written_test_path))
425
+ all_referenced_identifiers = _collect_referenced_identifiers(written_test_content)
426
+ for each_test_file_path in all_test_file_paths[:MAX_TEST_FILES_SCANNED]:
427
+ if os.path.normcase(str(each_test_file_path.resolve())) == written_path_key:
428
+ continue
429
+ try:
430
+ test_source = each_test_file_path.read_text(encoding="utf-8")
431
+ except (OSError, UnicodeDecodeError):
432
+ continue
433
+ all_referenced_identifiers |= _collect_referenced_identifiers(test_source)
434
+ return all_referenced_identifiers
435
+
436
+
437
+ def check_test_file_omits_module_public_function(
438
+ content: str,
439
+ file_path: str,
440
+ ) -> list[str]:
441
+ """Flag a public function the written test file's paired suite omits.
442
+
443
+ The companion of ``check_public_function_missing_paired_test`` for the
444
+ reverse write order: when a production module is written before its test
445
+ file, the production-side check sees no suite yet and the omission slips
446
+ through, so this check fires on the stem-matched test-file write instead. It
447
+ resolves the production module the written ``test_<stem>.py`` or
448
+ ``<stem>_test.py`` file pairs with, and when the post-edit suite already
449
+ covers at least one of that module's public functions, flags every public
450
+ function the suite references nowhere. ``main`` and underscore-prefixed
451
+ functions are never required to carry a test, and a production module that is
452
+ itself exempt — a test module, hook infrastructure, config module, migration,
453
+ workflow registry, or ``__init__`` module — is skipped.
454
+
455
+ A stem-matched test-file write authors the whole pairing relationship, so
456
+ every omission is reported unscoped: the gap lives in the test file under
457
+ edit, while the omitted function's definition line lives in the production
458
+ module — a different file whose line numbers are not addressable against the
459
+ test file's changed lines. Diff-line intersection would compare the two
460
+ unrelated coordinate spaces, so this check returns each omission directly
461
+ and the commit/push gate treats it as blocking.
462
+
463
+ Args:
464
+ content: The reconstructed post-edit whole-file content of the test file.
465
+ file_path: The destination path of the written test file, used to resolve
466
+ its paired production module on disk.
467
+
468
+ Returns:
469
+ One violation per public function the suite omits, capped at the
470
+ configured maximum. Empty when the file is not a stem-matched test file,
471
+ no paired production module exists on disk, the production module is
472
+ exempt or defines no public function, or the suite covers none of the
473
+ module's public functions.
474
+ """
475
+ production_stem = _production_stem_from_test_filename(Path(file_path).name)
476
+ if production_stem is None:
477
+ return []
478
+ test_path = Path(file_path).resolve()
479
+ production_module_path = _paired_production_module_path(test_path, production_stem)
480
+ if production_module_path is None:
481
+ return []
482
+ if _module_is_exempt(str(production_module_path)):
483
+ return []
484
+ try:
485
+ production_source = production_module_path.read_text(encoding="utf-8")
486
+ except (OSError, UnicodeDecodeError):
487
+ return []
488
+ try:
489
+ production_tree = ast.parse(production_source)
490
+ except SyntaxError:
491
+ return []
492
+ all_public_definitions = _public_function_definitions(production_tree)
493
+ if not all_public_definitions:
494
+ return []
495
+ all_referenced_identifiers = _post_edit_suite_referenced_identifiers(
496
+ test_path.parent, test_path, content
497
+ )
498
+ covered_function_count = sum(
499
+ 1
500
+ for each_name, _each_line in all_public_definitions
501
+ if each_name in all_referenced_identifiers
502
+ )
503
+ if covered_function_count < MINIMUM_COVERED_PUBLIC_FUNCTIONS:
504
+ return []
505
+ production_module_name = production_module_path.name
506
+ all_omission_violations: list[str] = []
507
+ for each_name, _each_definition_line in all_public_definitions:
508
+ if each_name in all_referenced_identifiers:
509
+ continue
510
+ all_omission_violations.append(
511
+ f"{production_module_name} public function "
512
+ f"{each_name!r} {TEST_SUITE_OMITS_FUNCTION_GUIDANCE}"
513
+ )
514
+ if len(all_omission_violations) >= MAX_PAIRED_TEST_COVERAGE_ISSUES:
515
+ break
516
+ return all_omission_violations
517
+
@@ -1,4 +1,4 @@
1
- """Skip-decorator, existence-only, constant-equality, stale-test-name, and flag-gated scenario test-quality checks."""
1
+ """Skip-decorator, existence-only, constant-equality, stale-test-name, flag-gated scenario, and vacuous cleanup-assertion test-quality checks."""
2
2
 
3
3
  import ast
4
4
  import sys
@@ -6,6 +6,37 @@ from pathlib import Path
6
6
 
7
7
  _SCENARIO_NAME_CLAUSES = ("_when_", "_passes", "_succeeds", "_on_clean")
8
8
  _MINIMUM_SIBLING_PATCH_COUNT = 2
9
+ _CLEANUP_REMOVAL_NAME_TOKENS = (
10
+ "remove",
11
+ "removes",
12
+ "removed",
13
+ "cleanup",
14
+ "clean_up",
15
+ "deletes",
16
+ "discards",
17
+ "no_tmp",
18
+ "no_temp",
19
+ "no_leftover",
20
+ "leftover",
21
+ )
22
+ _FAILURE_CONDITION_NAME_TOKENS = (
23
+ "on_failure",
24
+ "_failure",
25
+ "on_error",
26
+ "_fails",
27
+ "_failed",
28
+ "raises",
29
+ "bad_source",
30
+ )
31
+ _ARRANGEMENT_METHOD_NAMES = (
32
+ "setattr",
33
+ "mkdir",
34
+ "write_text",
35
+ "write_bytes",
36
+ "touch",
37
+ "symlink_to",
38
+ "exists",
39
+ )
9
40
 
10
41
  _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
11
42
  _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
@@ -233,6 +264,133 @@ def check_constant_equality_tests(content: str, file_path: str) -> list[str]:
233
264
  return issues
234
265
 
235
266
 
267
+ def _name_signals_cleanup_on_failure(test_name: str) -> bool:
268
+ """Return True when a test name pairs a removal token with a failure token."""
269
+ lowered_name = test_name.lower()
270
+ has_removal_token = any(
271
+ each_token in lowered_name for each_token in _CLEANUP_REMOVAL_NAME_TOKENS
272
+ )
273
+ has_failure_token = any(
274
+ each_token in lowered_name for each_token in _FAILURE_CONDITION_NAME_TOKENS
275
+ )
276
+ return has_removal_token and has_failure_token
277
+
278
+
279
+ def _is_absence_assertion(assert_node: ast.Assert) -> bool:
280
+ """Return True when an assertion only checks that a collection is empty."""
281
+ test_expr = assert_node.test
282
+ if isinstance(test_expr, ast.UnaryOp) and isinstance(test_expr.op, ast.Not):
283
+ return True
284
+ if not isinstance(test_expr, ast.Compare):
285
+ return False
286
+ if len(test_expr.ops) != 1 or not isinstance(test_expr.ops[0], ast.Eq):
287
+ return False
288
+ comparator = test_expr.comparators[0]
289
+ if (
290
+ isinstance(comparator, ast.Constant)
291
+ and isinstance(comparator.value, int)
292
+ and not isinstance(comparator.value, bool)
293
+ and comparator.value == 0
294
+ ):
295
+ return True
296
+ return isinstance(comparator, ast.List) and not comparator.elts
297
+
298
+
299
+ def _body_calls_attribute(
300
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef, attribute_name: str
301
+ ) -> bool:
302
+ """Return True when the function body calls a method with the given attribute name."""
303
+ for each_node in ast.walk(function_node):
304
+ if not isinstance(each_node, ast.Call):
305
+ continue
306
+ callee = each_node.func
307
+ if isinstance(callee, ast.Attribute) and callee.attr == attribute_name:
308
+ return True
309
+ return False
310
+
311
+
312
+ def _body_arranges_temp_existence(
313
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
314
+ ) -> bool:
315
+ """Return True when the body creates a temp file or arranges a post-creation failure.
316
+
317
+ A sound cleanup-on-failure test proves the temp existed before asserting its
318
+ removal: it monkeypatches the operation to fail after the temp is created, or
319
+ it creates the temp directly and checks its presence. Either signal means the
320
+ leftover-absence assertion is not vacuous.
321
+
322
+ Args:
323
+ function_node: The test function whose body is inspected.
324
+
325
+ Returns:
326
+ True when the body references ``monkeypatch`` or calls a file-creation or
327
+ existence method that establishes the temp file before the cleanup runs.
328
+ """
329
+ for each_node in ast.walk(function_node):
330
+ if isinstance(each_node, ast.Name) and each_node.id == "monkeypatch":
331
+ return True
332
+ if not isinstance(each_node, ast.Call):
333
+ continue
334
+ callee = each_node.func
335
+ if isinstance(callee, ast.Attribute) and callee.attr in _ARRANGEMENT_METHOD_NAMES:
336
+ return True
337
+ return False
338
+
339
+
340
+ def check_vacuous_cleanup_assertion_tests(content: str, file_path: str) -> list[str]:
341
+ """Flag a cleanup-on-failure test whose leftover-absence assertion is vacuous.
342
+
343
+ A test named for removal on failure that globs a directory and asserts the
344
+ result is empty, yet never creates the temp file nor arranges a failure after
345
+ the temp exists, passes even when the on-failure cleanup is entirely broken —
346
+ the leftover set is empty because nothing ever staged a leftover. The gate
347
+ fires when the test name pairs a removal token with a failure token, the body
348
+ globs a directory, every assertion only checks emptiness, and the body neither
349
+ monkeypatches a post-creation failure nor creates and checks the temp first.
350
+ Only applies to test files; production files are exempt.
351
+
352
+ Args:
353
+ content: The file body under validation.
354
+ file_path: Path to the file, used for the test-file gate.
355
+
356
+ Returns:
357
+ One issue per test whose cleanup-on-failure assertion passes vacuously.
358
+ """
359
+ if not is_test_file(file_path):
360
+ return []
361
+ try:
362
+ syntax_tree = ast.parse(content)
363
+ except SyntaxError:
364
+ return []
365
+
366
+ issues: list[str] = []
367
+ for each_node in ast.walk(syntax_tree):
368
+ if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
369
+ continue
370
+ if not each_node.name.startswith("test"):
371
+ continue
372
+ if not _name_signals_cleanup_on_failure(each_node.name):
373
+ continue
374
+ if not _body_calls_attribute(each_node, "glob"):
375
+ continue
376
+ if _body_arranges_temp_existence(each_node):
377
+ continue
378
+ body_assertions = _collect_body_assertions(each_node.body)
379
+ if not body_assertions:
380
+ continue
381
+ if not all(_is_absence_assertion(each_assert) for each_assert in body_assertions):
382
+ continue
383
+ issues.append(
384
+ f"Line {each_node.lineno}: vacuous cleanup-on-failure test"
385
+ f" — asserts no leftover temp file but never proves the temp was created,"
386
+ f" so it passes even when cleanup is broken; arrange a post-creation"
387
+ f" failure (e.g. monkeypatch os.replace to raise after the temp exists)"
388
+ f" then assert the temp was removed"
389
+ )
390
+
391
+ return issues
392
+
393
+
236
394
  def _flag_symbol_from_setattr_target(target_node: ast.expr) -> str | None:
237
395
  """Return the UPPER_SNAKE flag symbol a monkeypatch.setattr target names.
238
396
 
@@ -20,13 +20,10 @@ from code_rules_shared import ( # noqa: E402
20
20
  _build_parent_map,
21
21
  is_migration_file,
22
22
  is_test_file,
23
- is_workflow_registry_file,
24
23
  )
25
24
 
26
25
  from hooks_constants.unused_module_import_constants import ( # noqa: E402
27
- ALL_TYPING_MODULE_NAMES,
28
26
  MAX_UNUSED_IMPORT_ISSUES,
29
- TYPE_CHECKING_IDENTIFIER,
30
27
  UNUSED_IMPORT_GUIDANCE,
31
28
  line_suppresses_unused_import_via_noqa,
32
29
  )
@@ -73,59 +70,6 @@ def _line_number_falls_in_import_ranges(
73
70
  return False
74
71
 
75
72
 
76
- def _type_checking_guard_aliases(tree: ast.Module) -> tuple[set[str], set[str]]:
77
- all_type_checking_names = {TYPE_CHECKING_IDENTIFIER}
78
- all_type_checking_module_aliases = set(ALL_TYPING_MODULE_NAMES)
79
- for each_statement in tree.body:
80
- if isinstance(each_statement, ast.Import):
81
- for each_alias in each_statement.names:
82
- if each_alias.name in ALL_TYPING_MODULE_NAMES:
83
- all_type_checking_module_aliases.add(
84
- each_alias.asname or each_alias.name
85
- )
86
- elif isinstance(each_statement, ast.ImportFrom):
87
- if each_statement.module not in ALL_TYPING_MODULE_NAMES:
88
- continue
89
- for each_alias in each_statement.names:
90
- if each_alias.name == TYPE_CHECKING_IDENTIFIER:
91
- all_type_checking_names.add(each_alias.asname or each_alias.name)
92
- return all_type_checking_names, all_type_checking_module_aliases
93
-
94
-
95
- def _expression_guards_type_checking_block(
96
- test_expression: ast.expr,
97
- all_type_checking_names: set[str],
98
- all_type_checking_module_aliases: set[str],
99
- ) -> bool:
100
- if isinstance(test_expression, ast.Name):
101
- return test_expression.id in all_type_checking_names
102
- if isinstance(test_expression, ast.Attribute):
103
- if test_expression.attr != TYPE_CHECKING_IDENTIFIER:
104
- return False
105
- receiver = test_expression.value
106
- return (
107
- isinstance(receiver, ast.Name)
108
- and receiver.id in all_type_checking_module_aliases
109
- )
110
- return False
111
-
112
-
113
- def _module_body_declares_type_checking_gate(tree: ast.Module) -> bool:
114
- (
115
- all_type_checking_names,
116
- all_type_checking_module_aliases,
117
- ) = _type_checking_guard_aliases(tree)
118
- return any(
119
- isinstance(each_statement, ast.If)
120
- and _expression_guards_type_checking_block(
121
- each_statement.test,
122
- all_type_checking_names,
123
- all_type_checking_module_aliases,
124
- )
125
- for each_statement in tree.body
126
- )
127
-
128
-
129
73
  def _collect_load_names_outside_import_ranges(
130
74
  tree: ast.Module,
131
75
  all_import_line_ranges: list[tuple[int, int]],
@@ -191,10 +135,12 @@ def check_unused_module_level_imports(
191
135
 
192
136
  References are detected from AST ``Name`` / ``Attribute`` loads outside import
193
137
  statements so mentions in comments or string literals do not count. Files
194
- declaring ``__all__`` (including annotated assignments) are skipped. Files
195
- whose module body includes ``if TYPE_CHECKING:`` (or
196
- ``typing[._extensions].TYPE_CHECKING``) are skipped. Suppression honors bare
197
- ``# noqa`` or an explicit ``F401`` code in the noqa list only.
138
+ declaring ``__all__`` (including annotated assignments) are skipped. A
139
+ ``if TYPE_CHECKING:`` block does not exempt the file: its guarded imports
140
+ are nested inside the ``If`` node and are never scanned as top-level
141
+ bindings, while a dead top-level runtime import in the same file is still
142
+ flagged. Suppression honors bare ``# noqa`` or an explicit ``F401`` code in
143
+ the noqa list only.
198
144
 
199
145
  When ``full_file_content`` is provided, ``content`` is treated as an Edit
200
146
  fragment containing the imports being added or replaced, while the
@@ -205,7 +151,7 @@ def check_unused_module_level_imports(
205
151
  """
206
152
  if is_test_file(file_path):
207
153
  return []
208
- if is_workflow_registry_file(file_path) or is_migration_file(file_path):
154
+ if is_migration_file(file_path):
209
155
  return []
210
156
  try:
211
157
  fragment_tree = ast.parse(content)
@@ -218,8 +164,6 @@ def check_unused_module_level_imports(
218
164
  return []
219
165
  if _module_declares_dunder_all(reference_tree):
220
166
  return []
221
- if _module_body_declares_type_checking_gate(reference_tree):
222
- return []
223
167
  fragment_lines = content.splitlines()
224
168
  reference_import_ranges = _import_statement_line_ranges(reference_tree)
225
169
  referenced_names = _collect_load_names_outside_import_ranges(