claude-dev-env 1.71.0 → 1.73.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 (68) hide show
  1. package/CLAUDE.md +8 -0
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +5 -3
  3. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +39 -0
  4. package/agents/clean-coder.md +1 -0
  5. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +2 -2
  6. package/bin/install.mjs +73 -5
  7. package/bin/install.test.mjs +360 -4
  8. package/docs/CODE_RULES.md +1 -1
  9. package/hooks/blocking/CLAUDE.md +3 -1
  10. package/hooks/blocking/claude_md_orphan_file_blocker.py +5 -6
  11. package/hooks/blocking/code_rules_dead_config_field.py +69 -56
  12. package/hooks/blocking/code_rules_docstrings.py +676 -0
  13. package/hooks/blocking/code_rules_enforcer.py +26 -0
  14. package/hooks/blocking/code_rules_shared.py +19 -0
  15. package/hooks/blocking/code_rules_test_assertions.py +152 -1
  16. package/hooks/blocking/code_rules_type_escape.py +447 -2
  17. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +420 -0
  18. package/hooks/blocking/md_to_html_blocker.py +7 -8
  19. package/hooks/blocking/open_questions_in_plans_blocker.py +5 -6
  20. package/hooks/blocking/plain_language_blocker.py +51 -16
  21. package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -5
  22. package/hooks/blocking/pre_tool_use_dispatcher.py +545 -0
  23. package/hooks/blocking/pytest_testpaths_orphan_blocker.py +358 -0
  24. package/hooks/blocking/state_description_blocker.py +75 -36
  25. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +81 -0
  26. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +93 -0
  27. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +93 -0
  28. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +262 -0
  29. package/hooks/blocking/test_code_rules_enforcer_docstring_undefined_constant.py +253 -0
  30. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +279 -0
  31. package/hooks/blocking/test_code_rules_enforcer_object_parameter.py +499 -0
  32. package/hooks/blocking/test_code_rules_enforcer_stale_test_name.py +103 -0
  33. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +456 -0
  34. package/hooks/blocking/test_pre_tool_use_dispatcher.py +816 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +341 -0
  36. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +247 -0
  37. package/hooks/blocking/test_shared_stdin_adoption.py +166 -0
  38. package/hooks/blocking/verdict_directory_write_blocker.py +12 -7
  39. package/hooks/hooks.json +9 -79
  40. package/hooks/hooks_constants/CLAUDE.md +3 -1
  41. package/hooks/hooks_constants/blocking_check_limits.py +75 -0
  42. package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
  43. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +45 -0
  44. package/hooks/hooks_constants/dead_config_field_constants.py +5 -5
  45. package/hooks/hooks_constants/mypy_validator_cache_constants.py +36 -0
  46. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +69 -0
  47. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +135 -0
  48. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  49. package/hooks/hooks_constants/pytest_testpaths_orphan_blocker_constants.py +79 -0
  50. package/hooks/validation/mypy_validator.py +215 -17
  51. package/hooks/validation/post_tool_use_dispatcher.py +344 -0
  52. package/hooks/validation/test_mypy_validator.py +184 -1
  53. package/hooks/validation/test_post_tool_use_dispatcher.py +610 -0
  54. package/hooks/workflow/test_auto_formatter.py +10 -9
  55. package/package.json +1 -1
  56. package/rules/docstring-prose-matches-implementation.md +3 -2
  57. package/scripts/CLAUDE.md +1 -0
  58. package/scripts/Show-Asset.ps1 +106 -0
  59. package/skills/autoconverge/SKILL.md +123 -3
  60. package/skills/autoconverge/reference/convergence.md +41 -1
  61. package/skills/autoconverge/workflow/converge.contract.test.mjs +90 -0
  62. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +98 -0
  63. package/skills/autoconverge/workflow/converge.mjs +203 -8
  64. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +47 -0
  65. package/skills/autoconverge/workflow/converge_multi.mjs +161 -0
  66. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +100 -0
  67. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +47 -3
  68. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -0
@@ -69,6 +69,12 @@ from code_rules_docstrings import ( # noqa: E402
69
69
  check_docstring_args_match_signature,
70
70
  check_docstring_fallback_branch_coverage,
71
71
  check_docstring_format,
72
+ check_docstring_names_undefined_constant,
73
+ check_docstring_no_consumer_claim,
74
+ check_docstring_no_inline_literal_claim,
75
+ check_docstring_step_enumeration_dispatch_coverage,
76
+ check_docstring_tuple_enumeration_match,
77
+ check_module_docstring_names_public_checks,
72
78
  )
73
79
  from code_rules_duplicate_body import ( # noqa: E402
74
80
  advise_cross_skill_duplicate_helper,
@@ -122,6 +128,7 @@ from code_rules_test_assertions import ( # noqa: E402
122
128
  check_existence_check_tests,
123
129
  check_flag_gated_scenario_test_naming,
124
130
  check_skip_decorators_in_tests,
131
+ check_stale_test_name_target,
125
132
  )
126
133
  from code_rules_test_branching_except import ( # noqa: E402
127
134
  check_bare_except,
@@ -252,9 +259,27 @@ def validate_content(
252
259
  all_issues.extend(check_docstring_format(effective_content, file_path))
253
260
  all_issues.extend(check_docstring_args_match_signature(effective_content, file_path))
254
261
  all_issues.extend(check_docstring_fallback_branch_coverage(effective_content, file_path))
262
+ all_issues.extend(check_docstring_no_consumer_claim(effective_content, file_path))
263
+ all_issues.extend(
264
+ check_docstring_no_inline_literal_claim(effective_content, file_path)
265
+ )
255
266
  all_issues.extend(
256
267
  check_class_docstring_names_public_methods(effective_content, file_path)
257
268
  )
269
+ all_issues.extend(
270
+ check_module_docstring_names_public_checks(effective_content, file_path)
271
+ )
272
+ all_issues.extend(
273
+ check_docstring_tuple_enumeration_match(effective_content, file_path)
274
+ )
275
+ all_issues.extend(
276
+ check_docstring_step_enumeration_dispatch_coverage(
277
+ effective_content, file_path
278
+ )
279
+ )
280
+ all_issues.extend(
281
+ check_docstring_names_undefined_constant(effective_content, file_path)
282
+ )
258
283
  all_issues.extend(
259
284
  check_boolean_naming(
260
285
  effective_content,
@@ -282,6 +307,7 @@ def validate_content(
282
307
  )
283
308
  all_issues.extend(check_existence_check_tests(content, file_path))
284
309
  all_issues.extend(check_constant_equality_tests(content, file_path))
310
+ all_issues.extend(check_stale_test_name_target(content, file_path))
285
311
  check_flag_gated_scenario_test_naming(content, file_path)
286
312
  all_issues.extend(check_unused_optional_parameters(content, file_path))
287
313
  all_issues.extend(check_collection_prefix(content, file_path))
@@ -20,12 +20,14 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
20
20
  ALL_HOOK_INFRASTRUCTURE_PATTERNS,
21
21
  ALL_MIGRATION_PATH_PATTERNS,
22
22
  ALL_ROOT_ANCHORED_EPHEMERAL_DIRECTORIES,
23
+ ALL_STRICT_TEST_DIRECTORY_SEGMENTS,
23
24
  ALL_TEST_PATH_PATTERNS,
24
25
  ALL_WORKFLOW_REGISTRY_PATTERNS,
25
26
  CLAUDE_JOB_DIR_ENVIRONMENT_VARIABLE_NAME,
26
27
  CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY,
27
28
  EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME,
28
29
  LEADING_DRIVE_LETTER_PATTERN,
30
+ STRICT_TEST_FILE_BASENAME_PATTERN,
29
31
  )
30
32
  from hooks_constants.unused_module_import_constants import ( # noqa: E402
31
33
  TYPE_CHECKING_IDENTIFIER,
@@ -55,6 +57,23 @@ def is_test_file(file_path: str) -> bool:
55
57
  return any(pattern in path_lower for pattern in ALL_TEST_PATH_PATTERNS)
56
58
 
57
59
 
60
+ def is_strict_test_file(file_path: str) -> bool:
61
+ """Check if a file is a genuine test module by its basename, not a mid-name match.
62
+
63
+ A production module whose name carries the substring ``test`` mid-name —
64
+ such as ``code_rules_test_assertions.py`` — is not a test module. This
65
+ predicate anchors on the basename shape (``test_*`` / ``*_test.*`` /
66
+ ``*.test.*`` / ``*.spec.*`` / ``conftest.py``) or a ``/tests/`` path
67
+ segment, so it keeps such production modules out of the test exemption that
68
+ the substring-based is_test_file applies.
69
+ """
70
+ normalized_path = file_path.lower().replace("\\", "/")
71
+ if any(segment in normalized_path for segment in ALL_STRICT_TEST_DIRECTORY_SEGMENTS):
72
+ return True
73
+ basename_lower = normalized_path.rsplit("/", 1)[-1]
74
+ return STRICT_TEST_FILE_BASENAME_PATTERN.match(basename_lower) is not None
75
+
76
+
58
77
  def is_workflow_registry_file(file_path: str) -> bool:
59
78
  """Check if file is a workflow state/module registry file.
60
79
 
@@ -1,4 +1,4 @@
1
- """Skip-decorator, existence-only, constant-equality, and flag-gated scenario test-quality checks."""
1
+ """Skip-decorator, existence-only, constant-equality, stale-test-name, and flag-gated scenario test-quality checks."""
2
2
 
3
3
  import ast
4
4
  import sys
@@ -18,6 +18,10 @@ from code_rules_shared import ( # noqa: E402
18
18
  is_test_file,
19
19
  )
20
20
 
21
+ from hooks_constants.blocking_check_limits import ( # noqa: E402
22
+ MAX_STALE_TEST_NAME_TARGET_ISSUES,
23
+ STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT,
24
+ )
21
25
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
22
26
  UPPER_SNAKE_CONSTANT_PATTERN,
23
27
  )
@@ -346,3 +350,150 @@ def check_flag_gated_scenario_test_naming(content: str, file_path: str) -> list[
346
350
  )
347
351
 
348
352
  return []
353
+
354
+
355
+ def _called_function_names(function_node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
356
+ """Return the bare names of every function the test body calls."""
357
+ called_names: set[str] = set()
358
+ for each_node in ast.walk(function_node):
359
+ if not isinstance(each_node, ast.Call):
360
+ continue
361
+ callee = each_node.func
362
+ if isinstance(callee, ast.Name):
363
+ called_names.add(callee.id)
364
+ elif isinstance(callee, ast.Attribute):
365
+ called_names.add(callee.attr)
366
+ return called_names
367
+
368
+
369
+ def _module_known_callable_names(syntax_tree: ast.Module) -> set[str]:
370
+ """Return every callable-like name the module imports, defines, or calls.
371
+
372
+ A stale test name embeds a function that has been renamed away, so its name
373
+ appears nowhere as a real symbol. This set is the universe of names that DO
374
+ exist in the file, used to confirm the embedded name is absent.
375
+ """
376
+ known_names: set[str] = set()
377
+ for each_node in ast.walk(syntax_tree):
378
+ if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
379
+ known_names.add(each_node.name)
380
+ elif isinstance(each_node, ast.ImportFrom):
381
+ for each_alias in each_node.names:
382
+ known_names.add(each_alias.asname or each_alias.name)
383
+ elif isinstance(each_node, ast.Import):
384
+ for each_alias in each_node.names:
385
+ known_names.add((each_alias.asname or each_alias.name).split(".")[0])
386
+ elif isinstance(each_node, ast.Call):
387
+ callee = each_node.func
388
+ if isinstance(callee, ast.Name):
389
+ known_names.add(callee.id)
390
+ elif isinstance(callee, ast.Attribute):
391
+ known_names.add(callee.attr)
392
+ return known_names
393
+
394
+
395
+ def _leading_token_overlap(first_name: str, second_name: str) -> int:
396
+ """Return how many leading underscore-separated tokens two names share."""
397
+ first_tokens = first_name.split("_")
398
+ second_tokens = second_name.split("_")
399
+ shared = 0
400
+ for first_token, second_token in zip(first_tokens, second_tokens):
401
+ if first_token != second_token:
402
+ break
403
+ shared += 1
404
+ return shared
405
+
406
+
407
+ def _renamed_sibling_for_candidate(candidate_name: str, called_names: set[str]) -> str | None:
408
+ """Return a called function that looks like the renamed form of the candidate.
409
+
410
+ A rename keeps the token count and the leading tokens but swaps one or more
411
+ interior or trailing tokens (``collect_skip_theme_names`` to
412
+ ``collect_skip_clean_names``). The match requires an equal token count and a
413
+ shared leading run, which excludes an ordinary descriptive test suffix where
414
+ the called function is a strict shorter prefix of the embedded name.
415
+ """
416
+ candidate_token_count = len(candidate_name.split("_"))
417
+ for each_called in sorted(called_names):
418
+ if each_called == candidate_name:
419
+ continue
420
+ if len(each_called.split("_")) != candidate_token_count:
421
+ continue
422
+ if (
423
+ _leading_token_overlap(candidate_name, each_called)
424
+ >= STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT
425
+ ):
426
+ return each_called
427
+ return None
428
+
429
+
430
+ def _embedded_target_candidates(test_name: str) -> list[str]:
431
+ """Return the function-name candidates a test name embeds after its test_ prefix.
432
+
433
+ For ``test_collect_skip_theme_names_keeps_only_sorted_at_risk`` the candidates
434
+ are the successive leading runs ``collect_skip_theme_names``,
435
+ ``collect_skip_theme``, ``collect_skip`` — longest first — so the embedded
436
+ function name is matched before its shorter prefixes.
437
+ """
438
+ if not test_name.startswith("test_"):
439
+ return []
440
+ remainder_tokens = test_name[len("test_"):].split("_")
441
+ candidates: list[str] = []
442
+ for token_count in range(len(remainder_tokens), STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT - 1, -1):
443
+ candidates.append("_".join(remainder_tokens[:token_count]))
444
+ return candidates
445
+
446
+
447
+ def check_stale_test_name_target(content: str, file_path: str) -> list[str]:
448
+ """Flag a test whose name embeds a renamed-away function the body no longer calls.
449
+
450
+ When a producer function is renamed (``collect_skip_theme_names`` to
451
+ ``collect_skip_clean_names``) the test bodies are updated to call the new
452
+ name but the test function identifiers keep the old one. The result is a test
453
+ name advertising a function that exists nowhere in the file. This catches that
454
+ Category N test-name-versus-scenario drift: a ``test_*`` name embeds a
455
+ snake_case run of at least two tokens that names nothing the module imports,
456
+ defines, or calls, while the same test body calls a function sharing the
457
+ embedded run's leading tokens — the renamed sibling. Only applies to test
458
+ files; production files are exempt.
459
+
460
+ Args:
461
+ content: The file body under validation.
462
+ file_path: Path to the file, used for the test-file gate.
463
+
464
+ Returns:
465
+ One issue per test whose name embeds a renamed-away target, capped at the
466
+ module limit.
467
+ """
468
+ if not is_test_file(file_path):
469
+ return []
470
+ try:
471
+ syntax_tree = ast.parse(content)
472
+ except SyntaxError:
473
+ return []
474
+
475
+ known_names = _module_known_callable_names(syntax_tree)
476
+ issues: list[str] = []
477
+ for each_node in ast.walk(syntax_tree):
478
+ if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
479
+ continue
480
+ if not each_node.name.startswith("test"):
481
+ continue
482
+ called_names = _called_function_names(each_node)
483
+ for each_candidate in _embedded_target_candidates(each_node.name):
484
+ if each_candidate in known_names:
485
+ break
486
+ renamed_sibling = _renamed_sibling_for_candidate(each_candidate, called_names)
487
+ if renamed_sibling is None:
488
+ continue
489
+ issues.append(
490
+ f"Line {each_node.lineno}: test {each_node.name!r} names "
491
+ f"{each_candidate!r}, which the file never imports, defines, or calls; "
492
+ f"the body calls {renamed_sibling!r} instead — rename the test to match "
493
+ "the function it exercises (Category N test-name-vs-scenario drift)"
494
+ )
495
+ if len(issues) >= MAX_STALE_TEST_NAME_TARGET_ISSUES:
496
+ return issues[:MAX_STALE_TEST_NAME_TARGET_ISSUES]
497
+ break
498
+
499
+ return issues[:MAX_STALE_TEST_NAME_TARGET_ISSUES]