claude-dev-env 1.50.0 → 1.50.2

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 (82) hide show
  1. package/hooks/blocking/_gh_body_arg_utils.py +67 -11
  2. package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
  3. package/hooks/blocking/code_rules_annotations_length.py +167 -0
  4. package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
  5. package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
  6. package/hooks/blocking/code_rules_comments.py +337 -0
  7. package/hooks/blocking/code_rules_constants_config.py +252 -0
  8. package/hooks/blocking/code_rules_docstrings.py +308 -0
  9. package/hooks/blocking/code_rules_enforcer.py +98 -5765
  10. package/hooks/blocking/code_rules_imports_logging.py +276 -0
  11. package/hooks/blocking/code_rules_magic_values.py +180 -0
  12. package/hooks/blocking/code_rules_mock_completeness.py +295 -0
  13. package/hooks/blocking/code_rules_naming_collection.py +264 -0
  14. package/hooks/blocking/code_rules_optional_params.py +288 -0
  15. package/hooks/blocking/code_rules_paths_syspath.py +186 -0
  16. package/hooks/blocking/code_rules_probe_chains.py +305 -0
  17. package/hooks/blocking/code_rules_probe_detection.py +257 -0
  18. package/hooks/blocking/code_rules_probe_recording.py +225 -0
  19. package/hooks/blocking/code_rules_scope_binding.py +151 -0
  20. package/hooks/blocking/code_rules_shared.py +301 -0
  21. package/hooks/blocking/code_rules_string_magic.py +207 -0
  22. package/hooks/blocking/code_rules_test_assertions.py +226 -0
  23. package/hooks/blocking/code_rules_test_branching_except.py +181 -0
  24. package/hooks/blocking/code_rules_test_isolation.py +341 -0
  25. package/hooks/blocking/code_rules_type_escape.py +341 -0
  26. package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
  27. package/hooks/blocking/code_rules_unused_imports.py +256 -0
  28. package/hooks/blocking/conftest.py +30 -0
  29. package/hooks/blocking/pr_description_body_audit.py +148 -0
  30. package/hooks/blocking/pr_description_command_parser.py +233 -0
  31. package/hooks/blocking/pr_description_enforcer.py +36 -825
  32. package/hooks/blocking/pr_description_pr_number.py +153 -0
  33. package/hooks/blocking/pr_description_readability.py +366 -0
  34. package/hooks/blocking/tdd_enforcer.py +31 -0
  35. package/hooks/blocking/test_code_rules_constants_config.py +26 -0
  36. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
  37. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
  38. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
  39. package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
  40. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
  41. package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
  42. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
  43. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
  44. package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
  45. package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
  46. package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
  47. package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
  48. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
  49. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
  50. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
  53. package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
  54. package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
  55. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
  56. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
  57. package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
  58. package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
  59. package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
  60. package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
  61. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
  62. package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
  63. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
  64. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
  65. package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
  66. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
  67. package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
  68. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
  69. package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
  70. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
  71. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
  72. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
  73. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
  74. package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
  75. package/hooks/blocking/test_tdd_enforcer.py +116 -0
  76. package/hooks/hooks_constants/blocking_check_limits.py +3 -0
  77. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  78. package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
  79. package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
  80. package/package.json +1 -1
  81. package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
  82. package/hooks/blocking/test_md_to_html_blocker.py +0 -810
@@ -8,24 +8,28 @@ Covers:
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import importlib.util
12
- import io
13
11
  import sys
14
12
  from pathlib import Path
15
- from types import ModuleType
16
-
17
-
18
- def _load_enforcer_module() -> ModuleType:
19
- module_path = Path(__file__).parent / "code_rules_enforcer.py"
20
- spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
21
- assert spec is not None
22
- assert spec.loader is not None
23
- module = importlib.util.module_from_spec(spec)
24
- spec.loader.exec_module(module)
25
- return module
26
-
27
-
28
- code_rules_enforcer = _load_enforcer_module()
13
+ from types import SimpleNamespace
14
+
15
+ _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
16
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
17
+ if _BLOCKING_DIRECTORY not in sys.path:
18
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
19
+ if _HOOKS_DIRECTORY not in sys.path:
20
+ sys.path.insert(0, _HOOKS_DIRECTORY)
21
+
22
+ from code_rules_constants_config import ( # noqa: E402
23
+ check_constants_outside_config,
24
+ check_constants_outside_config_advisory,
25
+ )
26
+ from code_rules_path_utils import is_config_file # noqa: E402
27
+
28
+ code_rules_enforcer = SimpleNamespace(
29
+ check_constants_outside_config=check_constants_outside_config,
30
+ check_constants_outside_config_advisory=check_constants_outside_config_advisory,
31
+ is_config_file=is_config_file,
32
+ )
29
33
 
30
34
  PRODUCTION_FILE_PATH = "packages/claude-dev-env/src/example.py"
31
35
 
@@ -31,9 +31,11 @@ assert hook_spec is not None
31
31
  assert hook_spec.loader is not None
32
32
  hook_module = importlib.util.module_from_spec(hook_spec)
33
33
  hook_spec.loader.exec_module(hook_module)
34
- _is_exempt_python_comment = hook_module._is_exempt_python_comment
35
- check_comments_python = hook_module.check_comments_python
36
34
 
35
+ from code_rules_comments import ( # noqa: E402
36
+ _is_exempt_python_comment,
37
+ check_comments_python,
38
+ )
37
39
 
38
40
  FIXTURE_INLINE_COMMENT_LINE = 5
39
41
  FIXTURE_INLINE_COMMENT_COLUMN = 4
@@ -1,10 +1,15 @@
1
1
  """Tests for ``check_function_length``.
2
2
 
3
- Functions whose definition span (signature line through last body statement,
4
- inclusive) is at or above ``FUNCTION_LENGTH_BLOCKING_THRESHOLD`` (60) block the
5
- write (small-function basis: Robert C. Martin, Clean Code Ch. 3 "Functions";
6
- Google Python Style Guide ~40-line function review hint). Spans below the
7
- threshold pass silently.
3
+ Functions whose executable span (signature line through last body statement,
4
+ inclusive, minus the leading docstring lines of the function and of every
5
+ function or class nested within it) is at or above
6
+ ``FUNCTION_LENGTH_BLOCKING_THRESHOLD`` (60) block the write (small-function
7
+ basis: Robert C. Martin, Clean Code Ch. 3 "Functions"; Google Python Style
8
+ Guide ~40-line function review hint — a measure of executable complexity,
9
+ paired with the Guide's complete-docstring mandate for public APIs). Executable
10
+ spans below the threshold pass silently, whatever the docstring adds to the
11
+ full declared span; the issue message keeps reporting the full declared span so
12
+ the commit gate's span recovery holds.
8
13
 
9
14
  Cited SYNTHESIS evidence: pa#143 F4, F9, F14 (three recurrences in one PR);
10
15
  pa#136 F20.
@@ -12,23 +17,28 @@ pa#136 F20.
12
17
 
13
18
  from __future__ import annotations
14
19
 
15
- import importlib.util
16
20
  import pathlib
17
21
  import sys
22
+ from types import SimpleNamespace
23
+
24
+ _BLOCKING_DIRECTORY = str(pathlib.Path(__file__).resolve().parent)
25
+ _HOOKS_DIRECTORY = str(pathlib.Path(__file__).resolve().parent.parent)
26
+ if _BLOCKING_DIRECTORY not in sys.path:
27
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
28
+ if _HOOKS_DIRECTORY not in sys.path:
29
+ sys.path.insert(0, _HOOKS_DIRECTORY)
30
+
31
+ from code_rules_annotations_length import ( # noqa: E402
32
+ FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
33
+ FUNCTION_LENGTH_BLOCKING_THRESHOLD,
34
+ check_function_length,
35
+ )
18
36
 
19
- _HOOK_DIR = pathlib.Path(__file__).parent
20
- if str(_HOOK_DIR) not in sys.path:
21
- sys.path.insert(0, str(_HOOK_DIR))
22
-
23
- hook_spec = importlib.util.spec_from_file_location(
24
- "code_rules_enforcer",
25
- _HOOK_DIR / "code_rules_enforcer.py",
37
+ hook_module = SimpleNamespace(
38
+ FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX=FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
39
+ FUNCTION_LENGTH_BLOCKING_THRESHOLD=FUNCTION_LENGTH_BLOCKING_THRESHOLD,
40
+ check_function_length=check_function_length,
26
41
  )
27
- assert hook_spec is not None
28
- assert hook_spec.loader is not None
29
- hook_module = importlib.util.module_from_spec(hook_spec)
30
- hook_spec.loader.exec_module(hook_module)
31
- check_function_length = hook_module.check_function_length
32
42
 
33
43
  PRODUCTION_FILE_PATH = "/project/src/long_module.py"
34
44
  TEST_FILE_PATH = "/project/src/test_long_module.py"
@@ -208,3 +218,129 @@ def test_reports_only_in_scope_violation_among_untouched_ones() -> None:
208
218
  )
209
219
  assert any("target_function" in each_issue for each_issue in issues)
210
220
  assert not any("leading_" in each_issue for each_issue in issues)
221
+
222
+
223
+ def _build_docstring_function_source(
224
+ name: str, docstring_line_count: int, body_line_count: int
225
+ ) -> str:
226
+ """Build a function whose leading docstring spans ``docstring_line_count + 2``
227
+ source lines (opening summary line, the counted filler lines, closing quotes)
228
+ followed by ``body_line_count`` executable statements."""
229
+ docstring_lines = [
230
+ ' """Documented helper.',
231
+ *(
232
+ f" documentation line {each_index}."
233
+ for each_index in range(docstring_line_count)
234
+ ),
235
+ ' """',
236
+ ]
237
+ all_source_lines = [
238
+ f"def {name}() -> None:",
239
+ *docstring_lines,
240
+ *(
241
+ f" statement_{each_index} = {each_index}"
242
+ for each_index in range(body_line_count)
243
+ ),
244
+ ]
245
+ return "\n".join(all_source_lines) + "\n"
246
+
247
+
248
+ def test_docstring_heavy_function_with_small_body_passes() -> None:
249
+ """A complete Google-style docstring must not push a small-bodied function
250
+ over the gate: the threshold measures executable lines only."""
251
+ source = _build_docstring_function_source(
252
+ "documented_compact_helper",
253
+ docstring_line_count=hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD,
254
+ body_line_count=5,
255
+ )
256
+ issues = check_function_length(source, PRODUCTION_FILE_PATH)
257
+ assert issues == [], f"docstring lines must not count toward the gate, got: {issues!r}"
258
+
259
+
260
+ def test_oversized_executable_body_blocks_despite_docstring() -> None:
261
+ """A docstring does not acquit a genuinely large executable body, and the
262
+ issue message reports the full declared span so the commit gate's
263
+ ``function_length_span_range`` recovery keeps covering the whole function."""
264
+ docstring_line_count = 10
265
+ body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
266
+ source = _build_docstring_function_source(
267
+ "documented_oversized_helper",
268
+ docstring_line_count=docstring_line_count,
269
+ body_line_count=body_line_count,
270
+ )
271
+ full_declared_span = 1 + (docstring_line_count + 2) + body_line_count
272
+ issues = check_function_length(source, PRODUCTION_FILE_PATH)
273
+ assert any("documented_oversized_helper" in each_issue for each_issue in issues)
274
+ assert any(f"is {full_declared_span} lines" in each_issue for each_issue in issues)
275
+
276
+
277
+ def test_executable_span_boundary_sits_one_below_threshold() -> None:
278
+ """With the docstring excluded, an executable span of THRESHOLD - 1 passes
279
+ even though the full declared span sits far above the threshold."""
280
+ source = _build_docstring_function_source(
281
+ "documented_boundary_helper",
282
+ docstring_line_count=20,
283
+ body_line_count=hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 2,
284
+ )
285
+ issues = check_function_length(source, PRODUCTION_FILE_PATH)
286
+ assert issues == []
287
+
288
+
289
+ def test_builder_zero_docstring_line_count_keeps_span_contract() -> None:
290
+ """The builder's docstring-span contract (``docstring_line_count + 2``) holds
291
+ at the zero boundary, so hand-computed span oracles in tests cannot drift."""
292
+ source = _build_docstring_function_source(
293
+ "documented_minimal_helper", docstring_line_count=0, body_line_count=3
294
+ )
295
+ expected_total_lines = 1 + (0 + 2) + 3
296
+ assert len(source.splitlines()) == expected_total_lines
297
+
298
+
299
+ def test_builder_zero_body_line_count_keeps_span_contract() -> None:
300
+ """The builder's span contract holds at the zero-body boundary, so a
301
+ docstring-only function's hand-computed span oracle cannot drift."""
302
+ source = _build_docstring_function_source(
303
+ "documented_bodyless_helper", docstring_line_count=5, body_line_count=0
304
+ )
305
+ expected_total_lines = 1 + (5 + 2) + 0
306
+ assert len(source.splitlines()) == expected_total_lines
307
+
308
+
309
+ def test_nested_function_docstring_does_not_count_toward_outer() -> None:
310
+ """A nested helper's docstring is documentation too: the outer function's
311
+ executable span excludes every leading docstring within its declared span."""
312
+ nested_docstring_filler = "\n".join(
313
+ f" nested documentation line {each_index}."
314
+ for each_index in range(hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD)
315
+ )
316
+ source = (
317
+ "def outer_documented_orchestrator() -> None:\n"
318
+ " def nested_documented_helper() -> None:\n"
319
+ ' """Documented nested helper.\n'
320
+ f"{nested_docstring_filler}\n"
321
+ ' """\n'
322
+ " nested_statement = 1\n"
323
+ " outer_statement = 2\n"
324
+ )
325
+ issues = check_function_length(source, PRODUCTION_FILE_PATH)
326
+ assert issues == [], f"nested docstring lines must not count toward the gate, got: {issues!r}"
327
+
328
+
329
+ def test_nested_class_docstring_does_not_count_toward_outer() -> None:
330
+ """A nested class's docstring is documentation too: the outer function's
331
+ executable span excludes leading docstrings of nested classes as well."""
332
+ nested_class_docstring_filler = "\n".join(
333
+ f" nested class documentation line {each_index}."
334
+ for each_index in range(hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD)
335
+ )
336
+ source = (
337
+ "def outer_class_documented_orchestrator() -> None:\n"
338
+ " class NestedDocumentedConfig:\n"
339
+ ' """Documented nested class.\n'
340
+ f"{nested_class_docstring_filler}\n"
341
+ ' """\n'
342
+ " nested_field = 1\n"
343
+ " outer_statement = 2\n"
344
+ )
345
+ issues = check_function_length(source, PRODUCTION_FILE_PATH)
346
+ assert issues == [], f"nested class docstring lines must not count toward the gate, got: {issues!r}"
@@ -12,7 +12,6 @@ import importlib.util
12
12
  import pathlib
13
13
  import sys
14
14
 
15
-
16
15
  _HOOK_DIRECTORY = pathlib.Path(__file__).parent
17
16
  if str(_HOOK_DIRECTORY) not in sys.path:
18
17
  sys.path.insert(0, str(_HOOK_DIRECTORY))
@@ -26,8 +25,8 @@ assert _hook_spec.loader is not None
26
25
  _hook_module = importlib.util.module_from_spec(_hook_spec)
27
26
  _hook_spec.loader.exec_module(_hook_module)
28
27
  check_hardcoded_user_paths = _hook_module.check_hardcoded_user_paths
29
- HARDCODED_USER_PATH_PATTERN = _hook_module.HARDCODED_USER_PATH_PATTERN
30
28
 
29
+ from code_rules_paths_syspath import HARDCODED_USER_PATH_PATTERN # noqa: E402
31
30
 
32
31
  PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
33
32
  TEST_FILE_PATH = "packages/app/tests/test_loader.py"
@@ -7,22 +7,32 @@ call is exempt; only bare ``ast.Expr`` calls are flagged.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import importlib.util
10
+ import sys
11
11
  from pathlib import Path
12
- from types import ModuleType
12
+ from types import SimpleNamespace
13
13
 
14
+ _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
15
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
16
+ if _BLOCKING_DIRECTORY not in sys.path:
17
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
18
+ if _HOOKS_DIRECTORY not in sys.path:
19
+ sys.path.insert(0, _HOOKS_DIRECTORY)
14
20
 
15
- def _load_enforcer_module() -> ModuleType:
16
- module_path = Path(__file__).parent / "code_rules_enforcer.py"
17
- spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
18
- assert spec is not None
19
- assert spec.loader is not None
20
- module = importlib.util.module_from_spec(spec)
21
- spec.loader.exec_module(module)
22
- return module
23
-
21
+ from code_rules_boolean_mustcheck import ( # noqa: E402
22
+ MAX_IGNORED_MUST_CHECK_RETURN_ISSUES,
23
+ )
24
+ from code_rules_boolean_mustcheck import ( # noqa: E402
25
+ check_ignored_must_check_return as _check_ignored_must_check_return,
26
+ )
27
+ from code_rules_enforcer import ( # noqa: E402
28
+ validate_content as _validate_content,
29
+ )
24
30
 
25
- code_rules_enforcer = _load_enforcer_module()
31
+ code_rules_enforcer = SimpleNamespace(
32
+ MAX_IGNORED_MUST_CHECK_RETURN_ISSUES=MAX_IGNORED_MUST_CHECK_RETURN_ISSUES,
33
+ check_ignored_must_check_return=_check_ignored_must_check_return,
34
+ validate_content=_validate_content,
35
+ )
26
36
 
27
37
 
28
38
  def check_ignored_must_check_return(content: str, file_path: str) -> list[str]:
@@ -0,0 +1,55 @@
1
+ """Behavior tests for the code_rules_annotations_length code-rules check module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
10
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
11
+ if _BLOCKING_DIRECTORY not in sys.path:
12
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
13
+ if _HOOKS_DIRECTORY not in sys.path:
14
+ sys.path.insert(0, _HOOKS_DIRECTORY)
15
+
16
+ from code_rules_annotations_length import ( # noqa: E402
17
+ FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
18
+ check_function_length,
19
+ is_hook_infrastructure,
20
+ )
21
+
22
+ code_rules_enforcer = SimpleNamespace(
23
+ FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX=FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
24
+ check_function_length=check_function_length,
25
+ is_hook_infrastructure=is_hook_infrastructure,
26
+ )
27
+
28
+
29
+ def test_should_treat_repo_relative_hook_path_as_hook_infrastructure() -> None:
30
+ relative_hook_path = "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
31
+ assert code_rules_enforcer.is_hook_infrastructure(relative_hook_path) is True
32
+
33
+
34
+ def test_should_treat_backslash_repo_relative_hook_path_as_hook_infrastructure() -> None:
35
+ relative_hook_path = "packages\\claude-dev-env\\hooks\\blocking\\code_rules_enforcer.py"
36
+ assert code_rules_enforcer.is_hook_infrastructure(relative_hook_path) is True
37
+
38
+
39
+ def test_should_not_treat_unrelated_repo_relative_path_as_hook_infrastructure() -> None:
40
+ relative_source_path = "packages/claude-dev-env/skills/bugteam/scripts/runner.py"
41
+ assert code_rules_enforcer.is_hook_infrastructure(relative_source_path) is False
42
+
43
+
44
+ def test_should_exempt_repo_relative_hook_file_from_function_length() -> None:
45
+ body_lines = "\n".join(f" bound_{each_index} = {each_index}" for each_index in range(70))
46
+ grown_function_source = "def grown_function() -> None:\n" + body_lines + "\n"
47
+ relative_hook_path = "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
48
+ assert code_rules_enforcer.check_function_length(grown_function_source, relative_hook_path) == []
49
+
50
+
51
+ def test_function_length_message_does_not_cite_file_length_section() -> None:
52
+ """The blocking message must cite a function-length basis, not the
53
+ advisory file-length section (CODE_RULES §6.5)."""
54
+ assert "6.5" not in code_rules_enforcer.FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX
55
+ assert "Clean Code" in code_rules_enforcer.FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX
@@ -0,0 +1,170 @@
1
+ """Behavior tests for the code_rules_banned_identifiers code-rules check module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
10
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
11
+ if _BLOCKING_DIRECTORY not in sys.path:
12
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
13
+ if _HOOKS_DIRECTORY not in sys.path:
14
+ sys.path.insert(0, _HOOKS_DIRECTORY)
15
+
16
+ from code_rules_banned_identifiers import ( # noqa: E402
17
+ ALL_BANNED_IDENTIFIERS,
18
+ BANNED_IDENTIFIER_MESSAGE_SUFFIX,
19
+ BANNED_IDENTIFIER_SKIP_ADVISORY,
20
+ MAX_BANNED_IDENTIFIER_ISSUES,
21
+ check_banned_noun_word_boundary,
22
+ )
23
+
24
+ from hooks_constants.banned_identifiers_constants import ( # noqa: E402
25
+ ALL_BANNED_IDENTIFIERS as config_all_banned_identifiers,
26
+ )
27
+ from hooks_constants.banned_identifiers_constants import ( # noqa: E402
28
+ BANNED_IDENTIFIER_MESSAGE_SUFFIX as config_banned_identifier_message_suffix,
29
+ )
30
+ from hooks_constants.banned_identifiers_constants import ( # noqa: E402
31
+ BANNED_IDENTIFIER_SKIP_ADVISORY as config_banned_identifier_skip_advisory,
32
+ )
33
+ from hooks_constants.banned_identifiers_constants import ( # noqa: E402
34
+ MAX_BANNED_IDENTIFIER_ISSUES as config_max_banned_identifier_issues,
35
+ )
36
+
37
+ code_rules_enforcer = SimpleNamespace(
38
+ ALL_BANNED_IDENTIFIERS=ALL_BANNED_IDENTIFIERS,
39
+ BANNED_IDENTIFIER_MESSAGE_SUFFIX=BANNED_IDENTIFIER_MESSAGE_SUFFIX,
40
+ BANNED_IDENTIFIER_SKIP_ADVISORY=BANNED_IDENTIFIER_SKIP_ADVISORY,
41
+ MAX_BANNED_IDENTIFIER_ISSUES=MAX_BANNED_IDENTIFIER_ISSUES,
42
+ check_banned_noun_word_boundary=check_banned_noun_word_boundary,
43
+ )
44
+
45
+
46
+ def test_should_expose_all_banned_identifiers_from_config() -> None:
47
+ expected_banned_identifiers = frozenset({
48
+ "result", "data", "output", "response", "value", "item", "temp",
49
+ "argv", "args", "kwargs", "argc",
50
+ })
51
+ actual_banned_identifiers = getattr(
52
+ code_rules_enforcer, "ALL_BANNED_IDENTIFIERS", None
53
+ )
54
+ assert actual_banned_identifiers is not None, (
55
+ "Renamed constant ALL_BANNED_IDENTIFIERS must be importable from "
56
+ "config/banned_identifiers_constants.py and re-exposed on the "
57
+ f"enforcer module, got: {actual_banned_identifiers!r}"
58
+ )
59
+ assert expected_banned_identifiers <= actual_banned_identifiers, (
60
+ "ALL_BANNED_IDENTIFIERS must contain every expected banned identifier; "
61
+ f"missing: {expected_banned_identifiers - actual_banned_identifiers!r}"
62
+ )
63
+
64
+
65
+ def test_should_source_banned_identifier_companion_constants_from_config() -> None:
66
+ assert (
67
+ code_rules_enforcer.MAX_BANNED_IDENTIFIER_ISSUES
68
+ is config_max_banned_identifier_issues
69
+ )
70
+ assert (
71
+ code_rules_enforcer.BANNED_IDENTIFIER_MESSAGE_SUFFIX
72
+ is config_banned_identifier_message_suffix
73
+ )
74
+ assert (
75
+ code_rules_enforcer.BANNED_IDENTIFIER_SKIP_ADVISORY
76
+ is config_banned_identifier_skip_advisory
77
+ )
78
+
79
+
80
+ def test_should_reexport_all_banned_identifiers_from_config() -> None:
81
+ assert code_rules_enforcer.ALL_BANNED_IDENTIFIERS is config_all_banned_identifiers
82
+
83
+
84
+ def test_banned_noun_word_skips_non_aliased_upstream_import() -> None:
85
+ """A non-aliased upstream import the author cannot rename
86
+ (`from typing import ItemsView`) must not be flagged, while an
87
+ author-coined alias still is."""
88
+ production_path = "packages/myapp/services/customer_pipeline.py"
89
+ upstream_issues = code_rules_enforcer.check_banned_noun_word_boundary(
90
+ "from typing import ItemsView\n", production_path
91
+ )
92
+ aliased_issues = code_rules_enforcer.check_banned_noun_word_boundary(
93
+ "import legacy_helper as cached_response\n", production_path
94
+ )
95
+ assert upstream_issues == []
96
+ assert any("cached_response" in each_issue for each_issue in aliased_issues)
97
+
98
+
99
+ def test_banned_noun_word_defers_scope_to_caller_when_requested() -> None:
100
+ """loop7-P1: when the gate sets the deferral flag, the banned-noun check must
101
+ return every violation so ``split_violations_by_scope`` can scope by added
102
+ line before reporting the in-scope set."""
103
+ binding_count = 5
104
+ source = "".join(
105
+ f"BINDING_{each_index}_RESULT_PATH = {each_index}\n"
106
+ for each_index in range(binding_count)
107
+ )
108
+ issues = code_rules_enforcer.check_banned_noun_word_boundary(
109
+ source,
110
+ "/project/src/many_nouns.py",
111
+ defer_scope_to_caller=True,
112
+ )
113
+ assert len(issues) == binding_count, (
114
+ "deferral must return every banned-noun violation, "
115
+ f"got: {issues!r}"
116
+ )
117
+
118
+
119
+ def test_banned_noun_message_carries_binding_line_span() -> None:
120
+ """A banned-noun binding carries its own binding line as a one-line span so
121
+ the commit gate reconstructs it through the same shared span mechanism the
122
+ other diff-scoped checks use, while keeping the Line N: prefix intact. The
123
+ binding-line granularity matches the companion exact-match
124
+ check_banned_identifiers and avoids re-flagging a pre-existing binding when
125
+ an unrelated line of its enclosing function is edited."""
126
+ source = (
127
+ "def aggregate() -> list[int]:\n"
128
+ " canned_results = [1, 2, 3]\n"
129
+ " return canned_results\n"
130
+ )
131
+ issues = code_rules_enforcer.check_banned_noun_word_boundary(
132
+ source, "/project/src/has_noun.py"
133
+ )
134
+ binding_line = 2
135
+ expected_fragment = f"(binding span at line {binding_line}, spanning 1 lines)"
136
+ assert any(
137
+ each_issue.startswith(f"Line {binding_line}:") and expected_fragment in each_issue
138
+ for each_issue in issues
139
+ ), f"banned-noun message must carry the binding-line span fragment, got: {issues!r}"
140
+
141
+
142
+ def test_banned_noun_message_module_level_binding_spans_one_line() -> None:
143
+ """A module-level banned-noun binding spans its own binding line alone
144
+ (span 1)."""
145
+ source = "SAFE_OUTPUT_PATH = '/var/run/x'\n"
146
+ issues = code_rules_enforcer.check_banned_noun_word_boundary(
147
+ source, "/project/src/module_noun.py"
148
+ )
149
+ expected_fragment = "(binding span at line 1, spanning 1 lines)"
150
+ assert any(expected_fragment in each_issue for each_issue in issues), (
151
+ f"module-level banned-noun span must be one line, got: {issues!r}"
152
+ )
153
+
154
+
155
+ def test_banned_noun_word_boundary_flags_plural_results_identifier() -> None:
156
+ """A plural banned noun ('results') embedded in an identifier must flag.
157
+
158
+ ``ALL_BANNED_NOUN_WORDS`` contains plural forms (results, outputs,
159
+ responses, values, items) in addition to the singular nouns, so an
160
+ identifier such as ``canned_results`` is flagged even though no singular
161
+ exact-match identifier appears.
162
+ """
163
+ source = "canned_results = []\n"
164
+ issues = code_rules_enforcer.check_banned_noun_word_boundary(
165
+ source, "/project/src/pipeline.py"
166
+ )
167
+ assert any("canned_results" in each_issue for each_issue in issues), (
168
+ "a plural banned-noun identifier must be flagged by the word-boundary "
169
+ f"check; got: {issues!r}"
170
+ )
@@ -0,0 +1,60 @@
1
+ """Behavior tests for the code_rules_comments code-rules check module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
10
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
11
+ if _BLOCKING_DIRECTORY not in sys.path:
12
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
13
+ if _HOOKS_DIRECTORY not in sys.path:
14
+ sys.path.insert(0, _HOOKS_DIRECTORY)
15
+
16
+ from code_rules_comments import ( # noqa: E402
17
+ check_comments_python,
18
+ )
19
+
20
+ code_rules_enforcer = SimpleNamespace(
21
+ check_comments_python=check_comments_python,
22
+ )
23
+
24
+
25
+ def test_exempt_comment_rejects_noqa_prefixed_prose_lacking_boundary() -> None:
26
+ """A comment body that merely starts with `noqa` followed by non-boundary
27
+ characters is not a real noqa directive and must stay subject to the
28
+ no-new-comments rule."""
29
+ source = "x = compute() # noqa-but-not-really: explanation\n"
30
+ issues = code_rules_enforcer.check_comments_python(source)
31
+ assert issues
32
+
33
+
34
+ def test_exempt_comment_keeps_bare_and_coded_noqa_exempt() -> None:
35
+ """A bare `# noqa` and a coded `# noqa: E501` remain exempt under the
36
+ tightened boundary rule."""
37
+ bare_source = "x = compute() # noqa\n"
38
+ coded_source = "x = compute() # noqa: E501\n"
39
+ assert code_rules_enforcer.check_comments_python(bare_source) == []
40
+ assert code_rules_enforcer.check_comments_python(coded_source) == []
41
+
42
+
43
+ def test_exempt_comment_keeps_colon_terminated_markers_without_trailing_space() -> None:
44
+ """A colon-terminated marker (`pylint:`, `type:`, `pragma:`) is self-bounded
45
+ by its own colon, so the directive stays exempt even when the next character
46
+ follows the colon immediately."""
47
+ pylint_source = "import os # pylint:disable=unused-import\n"
48
+ type_ignore_source = "x = compute() # type:ignore\n"
49
+ pragma_source = "x = compute() # pragma:no-cover\n"
50
+ assert code_rules_enforcer.check_comments_python(pylint_source) == []
51
+ assert code_rules_enforcer.check_comments_python(type_ignore_source) == []
52
+ assert code_rules_enforcer.check_comments_python(pragma_source) == []
53
+
54
+
55
+ def test_exempt_comment_still_flags_noqa_glued_to_prose_without_boundary() -> None:
56
+ """The colon-terminated allowance must not loosen the boundary rule for
57
+ markers that do not end in a colon: `# noqaFOO` still lacks a real boundary
58
+ after `noqa` and stays subject to the no-new-comments rule."""
59
+ source = "x = compute() # noqaFOO\n"
60
+ assert code_rules_enforcer.check_comments_python(source)
@@ -0,0 +1,52 @@
1
+ """Behavior tests for the code_rules_path_utils code-rules check module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
10
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
11
+ if _BLOCKING_DIRECTORY not in sys.path:
12
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
13
+ if _HOOKS_DIRECTORY not in sys.path:
14
+ sys.path.insert(0, _HOOKS_DIRECTORY)
15
+
16
+ from code_rules_path_utils import is_config_file # noqa: E402
17
+ from validators.exempt_paths import ( # noqa: E402
18
+ is_config_file as exempt_paths_is_config_file,
19
+ )
20
+
21
+ code_rules_enforcer = SimpleNamespace(
22
+ is_config_file=is_config_file,
23
+ )
24
+
25
+
26
+ def test_is_config_file_rejects_filename_only_config_pattern() -> None:
27
+ """Paths where 'config' appears only in the filename (not as a directory segment) must return False."""
28
+ assert code_rules_enforcer.is_config_file("scripts/db/config.py") is False, (
29
+ "scripts/db/config.py — filename is config.py but parent dir is db, must be False"
30
+ )
31
+ assert code_rules_enforcer.is_config_file("lib/myconfig.py") is False, (
32
+ "lib/myconfig.py — config appears only in the filename stem, must be False"
33
+ )
34
+ assert code_rules_enforcer.is_config_file("src/app_config.py") is False, (
35
+ "src/app_config.py — config appears only in the filename stem, must be False"
36
+ )
37
+
38
+
39
+ def test_is_config_file_reexported_by_exempt_paths_matches_canonical() -> None:
40
+ """The exempt_paths re-export of is_config_file must agree with the canonical code_rules_path_utils implementation on all sample paths."""
41
+ all_sample_paths = [
42
+ "scripts/db/config.py",
43
+ "config/timing.py",
44
+ "settings.py",
45
+ ]
46
+ for each_path in all_sample_paths:
47
+ canonical_result = code_rules_enforcer.is_config_file(each_path)
48
+ exempt_paths_result = exempt_paths_is_config_file(each_path)
49
+ assert canonical_result == exempt_paths_result, (
50
+ f"is_config_file diverged for {each_path!r}: "
51
+ f"code_rules_path_utils={canonical_result}, exempt_paths={exempt_paths_result}"
52
+ )