claude-dev-env 1.77.0 → 1.79.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/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.test.mjs +3 -2
- package/hooks/blocking/CLAUDE.md +5 -2
- package/hooks/blocking/code_rules_dead_module_constant.py +215 -59
- package/hooks/blocking/code_rules_dead_split_branch.py +225 -0
- package/hooks/blocking/code_rules_docstrings.py +951 -6
- package/hooks/blocking/code_rules_enforcer.py +64 -0
- package/hooks/blocking/code_rules_naming_collection.py +76 -1
- package/hooks/blocking/code_rules_paired_test.py +517 -0
- package/hooks/blocking/code_rules_string_magic.py +71 -1
- package/hooks/blocking/code_rules_test_assertions.py +159 -1
- package/hooks/blocking/convergence_gate_blocker.py +24 -15
- package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +89 -2
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +105 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +262 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +226 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +339 -0
- package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_whitespace_indentation_magic.py +74 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +71 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/blocking_check_limits.py +102 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
- package/hooks/hooks_constants/paired_test_coverage_constants.py +35 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +2 -0
- package/rules/docstring-prose-matches-implementation.md +56 -53
- package/rules/env-var-table-code-drift.md +24 -0
- package/rules/file-global-constants.md +2 -2
- package/rules/package-inventory-stale-entry.md +4 -4
- package/rules/paired-test-coverage.md +35 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Skip-decorator, existence-only, constant-equality, stale-test-name,
|
|
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
|
|
|
@@ -68,6 +68,27 @@ def _resolve_owner_repo(cwd: str | None) -> tuple[str, str] | None:
|
|
|
68
68
|
return parts[0], parts[1]
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def _run_convergence_check(
|
|
72
|
+
script: str, owner: str, repo: str, pr_number: int, cwd: str | None
|
|
73
|
+
) -> subprocess.CompletedProcess[str]:
|
|
74
|
+
return subprocess.run(
|
|
75
|
+
[
|
|
76
|
+
sys.executable,
|
|
77
|
+
script,
|
|
78
|
+
"--owner",
|
|
79
|
+
owner,
|
|
80
|
+
"--repo",
|
|
81
|
+
repo,
|
|
82
|
+
"--pr-number",
|
|
83
|
+
str(pr_number),
|
|
84
|
+
],
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
cwd=cwd or None,
|
|
88
|
+
check=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
71
92
|
def main() -> None:
|
|
72
93
|
check_convergence_script = str(
|
|
73
94
|
Path.home() / ".claude/skills/pr-converge/scripts/check_convergence.py"
|
|
@@ -90,7 +111,7 @@ def main() -> None:
|
|
|
90
111
|
if not gh_pr_ready_pattern.search(command):
|
|
91
112
|
sys.exit(0)
|
|
92
113
|
|
|
93
|
-
cwd = hook_input.get("
|
|
114
|
+
cwd = hook_input.get("cwd")
|
|
94
115
|
pr_number = _resolve_pr_number(command, cwd)
|
|
95
116
|
if pr_number is None:
|
|
96
117
|
sys.exit(0)
|
|
@@ -100,20 +121,8 @@ def main() -> None:
|
|
|
100
121
|
sys.exit(0)
|
|
101
122
|
owner, repo = owner_repo
|
|
102
123
|
|
|
103
|
-
completed_process =
|
|
104
|
-
|
|
105
|
-
sys.executable,
|
|
106
|
-
check_convergence_script,
|
|
107
|
-
"--owner",
|
|
108
|
-
owner,
|
|
109
|
-
"--repo",
|
|
110
|
-
repo,
|
|
111
|
-
"--pr-number",
|
|
112
|
-
str(pr_number),
|
|
113
|
-
],
|
|
114
|
-
capture_output=True,
|
|
115
|
-
text=True,
|
|
116
|
-
check=False,
|
|
124
|
+
completed_process = _run_convergence_check(
|
|
125
|
+
check_convergence_script, owner, repo, pr_number, cwd
|
|
117
126
|
)
|
|
118
127
|
|
|
119
128
|
if completed_process.returncode in (0, 2):
|