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.
- package/CLAUDE.md +8 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +5 -3
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +39 -0
- package/agents/clean-coder.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +2 -2
- package/bin/install.mjs +73 -5
- package/bin/install.test.mjs +360 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +3 -1
- package/hooks/blocking/claude_md_orphan_file_blocker.py +5 -6
- package/hooks/blocking/code_rules_dead_config_field.py +69 -56
- package/hooks/blocking/code_rules_docstrings.py +676 -0
- package/hooks/blocking/code_rules_enforcer.py +26 -0
- package/hooks/blocking/code_rules_shared.py +19 -0
- package/hooks/blocking/code_rules_test_assertions.py +152 -1
- package/hooks/blocking/code_rules_type_escape.py +447 -2
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +420 -0
- package/hooks/blocking/md_to_html_blocker.py +7 -8
- package/hooks/blocking/open_questions_in_plans_blocker.py +5 -6
- package/hooks/blocking/plain_language_blocker.py +51 -16
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -5
- package/hooks/blocking/pre_tool_use_dispatcher.py +545 -0
- package/hooks/blocking/pytest_testpaths_orphan_blocker.py +358 -0
- package/hooks/blocking/state_description_blocker.py +75 -36
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +81 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +93 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +93 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +262 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_undefined_constant.py +253 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +279 -0
- package/hooks/blocking/test_code_rules_enforcer_object_parameter.py +499 -0
- package/hooks/blocking/test_code_rules_enforcer_stale_test_name.py +103 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +456 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +816 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +341 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +247 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +166 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +12 -7
- package/hooks/hooks.json +9 -79
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/blocking_check_limits.py +75 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +45 -0
- package/hooks/hooks_constants/dead_config_field_constants.py +5 -5
- package/hooks/hooks_constants/mypy_validator_cache_constants.py +36 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +69 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +135 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/pytest_testpaths_orphan_blocker_constants.py +79 -0
- package/hooks/validation/mypy_validator.py +215 -17
- package/hooks/validation/post_tool_use_dispatcher.py +344 -0
- package/hooks/validation/test_mypy_validator.py +184 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +610 -0
- package/hooks/workflow/test_auto_formatter.py +10 -9
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +3 -2
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Show-Asset.ps1 +106 -0
- package/skills/autoconverge/SKILL.md +123 -3
- package/skills/autoconverge/reference/convergence.md +41 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +90 -0
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +98 -0
- package/skills/autoconverge/workflow/converge.mjs +203 -8
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +47 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +161 -0
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +100 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +47 -3
- 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]
|