claude-dev-env 1.82.0 → 1.84.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 (217) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/audit-contract.md +24 -12
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/README.md +1 -0
  5. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  9. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  10. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  11. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  12. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  13. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  14. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  15. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  18. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  19. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  20. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  22. package/agents/clean-coder.md +3 -0
  23. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  24. package/bin/install.mjs +16 -8
  25. package/bin/install.test.mjs +17 -6
  26. package/commands/CLAUDE.md +0 -1
  27. package/docs/CODE_RULES.md +1 -1
  28. package/hooks/blocking/CLAUDE.md +2 -4
  29. package/hooks/blocking/code_rules_constants_config.py +164 -1
  30. package/hooks/blocking/code_rules_docstrings.py +428 -15
  31. package/hooks/blocking/code_rules_enforcer.py +33 -0
  32. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  33. package/hooks/blocking/code_rules_magic_values.py +5 -0
  34. package/hooks/blocking/code_rules_naming_collection.py +152 -6
  35. package/hooks/blocking/code_rules_shared.py +34 -0
  36. package/hooks/blocking/code_rules_string_magic.py +68 -0
  37. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  38. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  39. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  40. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  41. package/hooks/blocking/state_description_blocker.py +96 -5
  42. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  45. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  46. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  47. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  48. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  49. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  50. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  53. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  54. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  55. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  56. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  57. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  58. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  59. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  60. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  61. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  62. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  63. package/hooks/blocking/test_state_description_blocker.py +135 -0
  64. package/hooks/hooks.json +5 -0
  65. package/hooks/hooks_constants/CLAUDE.md +3 -4
  66. package/hooks/hooks_constants/blocking_check_limits.py +46 -0
  67. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  68. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  69. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  70. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  71. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  72. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  73. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  74. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  75. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  76. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  77. package/hooks/workflow/CLAUDE.md +2 -8
  78. package/package.json +1 -1
  79. package/rules/CLAUDE.md +5 -2
  80. package/rules/claude-md-orphan-file.md +5 -0
  81. package/rules/docstring-prose-matches-implementation.md +10 -1
  82. package/rules/env-var-table-code-drift.md +5 -0
  83. package/rules/es-exe-file-search.md +17 -0
  84. package/rules/no-historical-clutter.md +12 -1
  85. package/rules/orphan-css-class.md +5 -0
  86. package/rules/package-inventory-stale-entry.md +10 -0
  87. package/rules/paired-test-coverage.md +5 -0
  88. package/rules/plain-illustrative-docstrings.md +40 -1
  89. package/rules/verify-before-asking.md +7 -0
  90. package/rules/verify-runtime-state.md +40 -0
  91. package/rules/windows-filesystem-safe.md +8 -0
  92. package/rules/workers-done-before-complete.md +33 -0
  93. package/rules/workflow-substitution-slots.md +5 -0
  94. package/skills/CLAUDE.md +7 -2
  95. package/skills/autoconverge/SKILL.md +48 -61
  96. package/skills/autoconverge/reference/closing-report.md +6 -6
  97. package/skills/autoconverge/reference/convergence.md +17 -15
  98. package/skills/autoconverge/reference/gotchas.md +6 -3
  99. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  100. package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
  101. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
  102. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  103. package/skills/autoconverge/workflow/converge.mjs +167 -106
  104. package/skills/autoconverge/workflow/render_report.py +7 -11
  105. package/skills/bugteam/CLAUDE.md +1 -1
  106. package/skills/bugteam/PROMPTS.md +7 -6
  107. package/skills/bugteam/SKILL.md +25 -72
  108. package/skills/bugteam/reference/CLAUDE.md +1 -3
  109. package/skills/bugteam/reference/README.md +1 -1
  110. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  111. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  112. package/skills/bugteam/reference/team-setup.md +8 -7
  113. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  114. package/skills/bugteam/scripts/README.md +0 -4
  115. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  116. package/skills/code/SKILL.md +2 -0
  117. package/skills/copilot-review/CLAUDE.md +1 -1
  118. package/skills/copilot-review/SKILL.md +25 -23
  119. package/skills/everything-search/SKILL.md +5 -0
  120. package/skills/findbugs/CLAUDE.md +2 -2
  121. package/skills/findbugs/SKILL.md +22 -83
  122. package/skills/fixbugs/SKILL.md +2 -4
  123. package/skills/log-audit/CLAUDE.md +20 -0
  124. package/skills/log-audit/SKILL.md +68 -0
  125. package/skills/log-audit/reference/CLAUDE.md +9 -0
  126. package/skills/log-audit/reference/charter.md +52 -0
  127. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  128. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  129. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  130. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  131. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  132. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  133. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  134. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  135. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  136. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  137. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  138. package/skills/monitor-open-prs/SKILL.md +2 -2
  139. package/skills/post-audit-findings/SKILL.md +84 -0
  140. package/skills/pr-converge/CLAUDE.md +2 -0
  141. package/skills/pr-converge/SKILL.md +72 -59
  142. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  143. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  144. package/skills/pr-converge/reference/examples.md +5 -5
  145. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  146. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  147. package/skills/pr-converge/reference/per-tick.md +24 -45
  148. package/skills/pr-converge/reference/state-schema.md +15 -0
  149. package/skills/pr-converge/scripts/README.md +3 -5
  150. package/skills/pr-fix-protocol/SKILL.md +70 -0
  151. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  152. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  153. package/skills/pr-scope-resolve/SKILL.md +48 -0
  154. package/skills/qbug/CLAUDE.md +4 -4
  155. package/skills/qbug/SKILL.md +46 -144
  156. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  157. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  158. package/skills/reviewer-gates/SKILL.md +96 -0
  159. package/skills/session-log/CLAUDE.md +7 -7
  160. package/skills/session-log/SKILL.md +27 -44
  161. package/skills/test_markdown_link_integrity.py +103 -0
  162. package/commands/doc-gist.md +0 -16
  163. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  164. package/hooks/blocking/conftest.py +0 -30
  165. package/hooks/blocking/md_path_exemptions.py +0 -224
  166. package/hooks/blocking/md_to_html_blocker.py +0 -155
  167. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  168. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  169. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  170. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  171. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  172. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  173. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  174. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  175. package/hooks/workflow/md_to_html_companion.py +0 -358
  176. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  177. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  178. package/skills/bugteam/reference/audit-contract.md +0 -163
  179. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  181. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  182. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  183. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  184. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  185. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  186. package/skills/doc-gist/CLAUDE.md +0 -25
  187. package/skills/doc-gist/SKILL.md +0 -97
  188. package/skills/doc-gist/references/CLAUDE.md +0 -9
  189. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  190. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  191. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  192. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  193. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  194. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  195. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  196. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  197. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  198. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  199. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  200. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  201. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  202. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  203. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  204. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  205. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  206. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  207. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  208. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  209. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  210. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  211. package/skills/doc-gist/references/examples/README.md +0 -5
  212. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  213. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  214. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  215. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  216. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  217. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -93,6 +93,46 @@ def test_should_not_flag_module_docstring_naming_every_public_check() -> None:
93
93
  assert issues == [], f"Docstring naming every check must not flag, got: {issues!r}"
94
94
 
95
95
 
96
+ def _registry_module_omitting_check_via_shared_tokens_only() -> str:
97
+ return (
98
+ '"""Bare string-literal magic, inline literal-collection, inline tuple '
99
+ 'string-magic, and whitespace-indentation magic checks."""\n'
100
+ "\n"
101
+ "def check_string_literal_magic(content: str, file_path: str) -> list[str]:\n"
102
+ " return []\n"
103
+ "\n"
104
+ "def check_join_separator_string_magic(content: str, file_path: str) -> list[str]:\n"
105
+ " return []\n"
106
+ "\n"
107
+ "def check_inline_literal_collections(content: str, file_path: str) -> list[str]:\n"
108
+ " return []\n"
109
+ "\n"
110
+ "def check_inline_tuple_string_magic(content: str, file_path: str) -> list[str]:\n"
111
+ " return []\n"
112
+ "\n"
113
+ "def check_whitespace_indentation_magic(content: str, file_path: str) -> list[str]:\n"
114
+ " return []\n"
115
+ )
116
+
117
+
118
+ def test_should_flag_check_named_only_by_tokens_shared_with_siblings() -> None:
119
+ issues = check_module_docstring_names_public_checks(
120
+ _registry_module_omitting_check_via_shared_tokens_only(), HOOK_INFRASTRUCTURE_PATH
121
+ )
122
+ assert any("check_join_separator_string_magic" in each for each in issues), (
123
+ "A check whose only summary-present tokens ('string', 'magic') are shared with "
124
+ f"sibling checks must flag as omitted, got: {issues!r}"
125
+ )
126
+ assert not any("check_string_literal_magic" in each for each in issues), (
127
+ f"A check the summary names ('string-literal magic') must not flag, got: {issues!r}"
128
+ )
129
+ assert not any("check_inline_literal_collections" in each for each in issues), (
130
+ "A check named by a stemmed token ('collection' for 'collections') must not "
131
+ f"flag, got: {issues!r}"
132
+ )
133
+ assert len(issues) == 1
134
+
135
+
96
136
  def test_should_not_flag_module_with_a_single_public_check() -> None:
97
137
  source = (
98
138
  '"""Skip-decorator test-quality check."""\n'
@@ -0,0 +1,213 @@
1
+ """Tests for the naive-datetime-construction check in code_rules_imports_logging.py.
2
+
3
+ The check catches the DST-fold-ambiguity class Copilot flagged on
4
+ python-automation PR #557: ``datetime.fromtimestamp(epoch)`` builds a datetime
5
+ with no timezone, and a later ``.astimezone()`` on that naive value has to guess
6
+ the local offset. The check flags the naive-producing constructors
7
+ (``fromtimestamp``/``utcfromtimestamp`` without ``tz=``, and ``utcnow``) and
8
+ leaves a ``tz=``-carrying call, and a plain ``now()``, alone.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ 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()
29
+
30
+ PRODUCTION_FILE_PATH = "/project/src/export_verification.py"
31
+ TEST_FILE_PATH = "/project/src/test_export_verification.py"
32
+
33
+
34
+ def check_naive_datetime_construction(content: str, file_path: str) -> list[str]:
35
+ return code_rules_enforcer.check_naive_datetime_construction(content, file_path)
36
+
37
+
38
+ def test_flags_fromtimestamp_without_timezone() -> None:
39
+ content = (
40
+ "from datetime import datetime\n"
41
+ "def describe(stamp: float) -> str:\n"
42
+ " return datetime.fromtimestamp(stamp).astimezone().isoformat()\n"
43
+ )
44
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
45
+ assert len(issues) == 1
46
+ assert "fromtimestamp" in issues[0]
47
+ assert "tz=" in issues[0]
48
+
49
+
50
+ def test_flags_utcfromtimestamp_and_utcnow() -> None:
51
+ content = (
52
+ "from datetime import datetime\n"
53
+ "def build(stamp: float) -> tuple[object, object]:\n"
54
+ " return datetime.utcfromtimestamp(stamp), datetime.utcnow()\n"
55
+ )
56
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
57
+ assert len(issues) == 2
58
+
59
+
60
+ def test_flags_module_qualified_datetime_call() -> None:
61
+ content = (
62
+ "import datetime\n"
63
+ "def describe(stamp: float) -> object:\n"
64
+ " return datetime.datetime.fromtimestamp(stamp)\n"
65
+ )
66
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
67
+ assert len(issues) == 1
68
+
69
+
70
+ def test_allows_fromtimestamp_with_timezone_keyword() -> None:
71
+ content = (
72
+ "from datetime import datetime, timezone\n"
73
+ "def describe(stamp: float) -> str:\n"
74
+ " return datetime.fromtimestamp(stamp, tz=timezone.utc).isoformat()\n"
75
+ )
76
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
77
+ assert issues == []
78
+
79
+
80
+ def test_allows_fromtimestamp_with_positional_timezone() -> None:
81
+ content = (
82
+ "from datetime import datetime, timezone\n"
83
+ "def describe(stamp: float) -> str:\n"
84
+ " return datetime.fromtimestamp(stamp, timezone.utc).isoformat()\n"
85
+ )
86
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
87
+ assert issues == []
88
+
89
+
90
+ def test_allows_module_qualified_fromtimestamp_with_positional_timezone() -> None:
91
+ content = (
92
+ "import datetime\n"
93
+ "def describe(stamp: float) -> object:\n"
94
+ " return datetime.datetime.fromtimestamp(stamp, datetime.timezone.utc)\n"
95
+ )
96
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
97
+ assert issues == []
98
+
99
+
100
+ def test_flags_utcfromtimestamp_with_second_positional_argument() -> None:
101
+ content = (
102
+ "from datetime import datetime\n"
103
+ "def describe(stamp: float, other: float) -> object:\n"
104
+ " return datetime.utcfromtimestamp(stamp, other)\n"
105
+ )
106
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
107
+ assert len(issues) == 1
108
+
109
+
110
+ def test_ignores_unrelated_datetime_attribute_chain() -> None:
111
+ content = (
112
+ "def describe(foo: object, stamp: float) -> object:\n"
113
+ " return foo.datetime.fromtimestamp(stamp)\n"
114
+ )
115
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
116
+ assert issues == []
117
+
118
+
119
+ def test_flags_fromtimestamp_with_none_timezone_keyword() -> None:
120
+ content = (
121
+ "from datetime import datetime\n"
122
+ "def describe(stamp: float) -> object:\n"
123
+ " return datetime.fromtimestamp(stamp, tz=None)\n"
124
+ )
125
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
126
+ assert len(issues) == 1
127
+
128
+
129
+ def test_flags_fromtimestamp_with_none_positional_timezone() -> None:
130
+ content = (
131
+ "from datetime import datetime\n"
132
+ "def describe(stamp: float) -> object:\n"
133
+ " return datetime.fromtimestamp(stamp, None)\n"
134
+ )
135
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
136
+ assert len(issues) == 1
137
+
138
+
139
+ def test_flags_utcfromtimestamp_even_with_timezone_keyword() -> None:
140
+ content = (
141
+ "from datetime import datetime, timezone\n"
142
+ "def describe(stamp: float) -> object:\n"
143
+ " return datetime.utcfromtimestamp(stamp, tz=timezone.utc)\n"
144
+ )
145
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
146
+ assert len(issues) == 1
147
+
148
+
149
+ def test_utcnow_remediation_points_at_now_not_utcnow_tz() -> None:
150
+ content = (
151
+ "from datetime import datetime\n"
152
+ "def stamp() -> object:\n"
153
+ " return datetime.utcnow()\n"
154
+ )
155
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
156
+ assert len(issues) == 1
157
+ assert "now(tz=" in issues[0]
158
+
159
+
160
+ def test_utcfromtimestamp_remediation_points_at_fromtimestamp() -> None:
161
+ content = (
162
+ "from datetime import datetime\n"
163
+ "def build(stamp: float) -> object:\n"
164
+ " return datetime.utcfromtimestamp(stamp)\n"
165
+ )
166
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
167
+ assert len(issues) == 1
168
+ assert "fromtimestamp(" in issues[0]
169
+ assert "tz=" in issues[0]
170
+
171
+
172
+ def test_utcfromtimestamp_remediation_uses_generic_placeholder() -> None:
173
+ content = (
174
+ "from datetime import datetime\n"
175
+ "def build(epoch_seconds: float) -> object:\n"
176
+ " return datetime.utcfromtimestamp(epoch_seconds)\n"
177
+ )
178
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
179
+ assert len(issues) == 1
180
+ assert "..., tz=" in issues[0]
181
+ assert "stamp, tz=" not in issues[0]
182
+
183
+
184
+ def test_remediation_does_not_mandate_astimezone() -> None:
185
+ content = (
186
+ "from datetime import datetime\n"
187
+ "def stamp() -> object:\n"
188
+ " return datetime.utcnow()\n"
189
+ )
190
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
191
+ assert len(issues) == 1
192
+ assert "then .astimezone()" not in issues[0]
193
+ assert "only when local display" in issues[0]
194
+
195
+
196
+ def test_allows_plain_now() -> None:
197
+ content = (
198
+ "from datetime import datetime\n"
199
+ "def stamp() -> str:\n"
200
+ " return datetime.now().isoformat()\n"
201
+ )
202
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
203
+ assert issues == []
204
+
205
+
206
+ def test_exempts_test_files() -> None:
207
+ content = (
208
+ "from datetime import datetime\n"
209
+ "def describe(stamp: float) -> object:\n"
210
+ " return datetime.fromtimestamp(stamp)\n"
211
+ )
212
+ issues = check_naive_datetime_construction(content, TEST_FILE_PATH)
213
+ assert issues == []
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import importlib.util
5
+
6
+ ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
7
+ specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
8
+ code_rules_enforcer = importlib.util.module_from_spec(specification)
9
+ specification.loader.exec_module(code_rules_enforcer)
10
+
11
+ PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
12
+ CONFTEST_FILE_PATH = "packages/app/tests/conftest.py"
13
+
14
+
15
+ def test_should_flag_referenced_underscore_loop_variable_in_conftest() -> None:
16
+ source = (
17
+ "import sys\n"
18
+ "\n"
19
+ "for _foreign_module_name in ['a', 'b']:\n"
20
+ " del sys.modules[_foreign_module_name]\n"
21
+ )
22
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
23
+ source, CONFTEST_FILE_PATH
24
+ )
25
+ assert any("_foreign_module_name" in each_issue for each_issue in issues), (
26
+ f"Expected referenced underscore loop variable flagged, got: {issues}"
27
+ )
28
+
29
+
30
+ def test_should_flag_referenced_underscore_loop_variable_in_production() -> None:
31
+ source = (
32
+ "import sys\n"
33
+ "\n"
34
+ "def purge(all_names: list) -> None:\n"
35
+ " for _name in all_names:\n"
36
+ " del sys.modules[_name]\n"
37
+ )
38
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
39
+ source, PRODUCTION_FILE_PATH
40
+ )
41
+ assert any("_name" in each_issue for each_issue in issues), (
42
+ f"Expected referenced underscore loop variable flagged, got: {issues}"
43
+ )
44
+
45
+
46
+ def test_should_not_flag_unreferenced_underscore_throwaway() -> None:
47
+ source = "def spin(count: int) -> None:\n for _unused in range(count):\n pass\n"
48
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
49
+ source, PRODUCTION_FILE_PATH
50
+ )
51
+ assert issues == [], f"Genuinely unused underscore throwaway must pass, got: {issues}"
52
+
53
+
54
+ def test_should_not_flag_bare_underscore_target() -> None:
55
+ source = "def spin(count: int) -> None:\n for _ in range(count):\n return None\n"
56
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
57
+ source, PRODUCTION_FILE_PATH
58
+ )
59
+ assert issues == [], f"Bare '_' target must pass, got: {issues}"
60
+
61
+
62
+ def test_should_not_flag_named_loop_variable_without_underscore() -> None:
63
+ source = (
64
+ "import sys\n\nfor each_module_name in ['a', 'b']:\n del sys.modules[each_module_name]\n"
65
+ )
66
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
67
+ source, CONFTEST_FILE_PATH
68
+ )
69
+ assert issues == [], f"each_-prefixed loop variable without underscore must pass, got: {issues}"
70
+
71
+
72
+ def test_should_not_flag_underscore_loop_variable_shadowed_by_comprehension() -> None:
73
+ source = (
74
+ "def collect(all_names: list, other: list) -> list:\n"
75
+ " all_other: list = []\n"
76
+ " for _name in all_names:\n"
77
+ " all_other = [_name for _name in other]\n"
78
+ " return all_other\n"
79
+ )
80
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
81
+ source, PRODUCTION_FILE_PATH
82
+ )
83
+ assert issues == [], (
84
+ f"Comprehension rebinding the loop name shadows the outer read, must pass, got: {issues}"
85
+ )
86
+
87
+
88
+ def test_should_flag_underscore_loop_variable_read_inside_non_rebinding_comprehension() -> None:
89
+ source = (
90
+ "def collect(all_names: list, other: list) -> list:\n"
91
+ " matches: list = []\n"
92
+ " for _name in all_names:\n"
93
+ " matches = [each_row for each_row in other if each_row == _name]\n"
94
+ " return matches\n"
95
+ )
96
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
97
+ source, PRODUCTION_FILE_PATH
98
+ )
99
+ assert any("_name" in each_issue for each_issue in issues), (
100
+ f"A comprehension reading the outer loop name without rebinding it must flag, got: {issues}"
101
+ )
102
+
103
+
104
+ def test_should_not_flag_underscore_loop_variable_shadowed_by_lambda_parameter() -> None:
105
+ source = (
106
+ "def build(all_names: list) -> list:\n"
107
+ " all_callables: list = []\n"
108
+ " for _name in all_names:\n"
109
+ " all_callables.append(lambda _name: _name)\n"
110
+ " return all_callables\n"
111
+ )
112
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
113
+ source, PRODUCTION_FILE_PATH
114
+ )
115
+ assert issues == [], (
116
+ f"A lambda parameter shadowing the loop name owns its body, must pass, got: {issues}"
117
+ )
118
+
119
+
120
+ def test_should_not_flag_underscore_loop_variable_shadowed_by_nested_def_parameter() -> None:
121
+ source = (
122
+ "def build(all_names: list) -> None:\n"
123
+ " for _handler in all_names:\n"
124
+ " def inner(_handler: object) -> object:\n"
125
+ " return _handler\n"
126
+ )
127
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
128
+ source, PRODUCTION_FILE_PATH
129
+ )
130
+ assert issues == [], (
131
+ f"A nested-def parameter shadowing the loop name owns its body, must pass, got: {issues}"
132
+ )
133
+
134
+
135
+ def test_should_not_flag_outer_underscore_loop_variable_shadowed_by_nested_loop() -> None:
136
+ source = (
137
+ "import sys\n"
138
+ "\n"
139
+ "def purge(first: list, second: list) -> None:\n"
140
+ " for _name in first:\n"
141
+ " for _name in second:\n"
142
+ " del sys.modules[_name]\n"
143
+ )
144
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
145
+ source, PRODUCTION_FILE_PATH
146
+ )
147
+ assert not any("Line 4" in each_issue for each_issue in issues), (
148
+ f"Outer loop whose name a nested loop rebinds is never read in the outer scope, "
149
+ f"got: {issues}"
150
+ )
151
+
152
+
153
+ def test_should_flag_underscore_loop_variable_read_before_nested_shadowing_loop() -> None:
154
+ source = (
155
+ "import sys\n"
156
+ "\n"
157
+ "def purge(first: list, second: list) -> None:\n"
158
+ " for _name in first:\n"
159
+ " del sys.modules[_name]\n"
160
+ " for _name in second:\n"
161
+ " pass\n"
162
+ )
163
+ issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
164
+ source, PRODUCTION_FILE_PATH
165
+ )
166
+ assert any("Line 4" in each_issue for each_issue in issues), (
167
+ f"The outer loop body reads its own name before the nested loop rebinds it, "
168
+ f"must flag, got: {issues}"
169
+ )
@@ -177,6 +177,39 @@ def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
177
177
  )
178
178
 
179
179
 
180
+ def test_scan_function_body_constants_skips_docstring_contrast_line() -> None:
181
+ source = (
182
+ "def demo(label):\n"
183
+ ' """Flag a boolean assignment whose target and callee clash.\n'
184
+ "\n"
185
+ " ::\n"
186
+ "\n"
187
+ " FLAG: ready = make_stale(...)\n"
188
+ ' """\n'
189
+ " return label\n"
190
+ )
191
+ advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
192
+ assert advisory_issues == [], (
193
+ f"Expected no function-local constant issue on an illustrative row, got: {advisory_issues}"
194
+ )
195
+
196
+
197
+ def test_scan_function_body_constants_still_flags_real_function_body_constant() -> None:
198
+ source = (
199
+ "def demo(label):\n"
200
+ ' """Flag a boolean assignment whose target and callee clash.\n'
201
+ "\n"
202
+ " FLAG: ready = make_stale(...)\n"
203
+ ' """\n'
204
+ " SOME_CONST = 3\n"
205
+ " return SOME_CONST\n"
206
+ )
207
+ advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
208
+ assert any("SOME_CONST" in issue for issue in advisory_issues), (
209
+ f"Expected the real function-body constant SOME_CONST to stay flagged, got: {advisory_issues}"
210
+ )
211
+
212
+
180
213
  def test_check_constants_outside_config_flags_annotated_assignment() -> None:
181
214
  source = "TEXT_FILE_ENCODING: str = 'utf-8'\n"
182
215
  issues = code_rules_enforcer.check_constants_outside_config(
@@ -18,14 +18,37 @@ from code_rules_enforcer import ( # noqa: E402
18
18
  )
19
19
  from code_rules_shared import ( # noqa: E402
20
20
  changed_line_numbers,
21
+ docstring_line_numbers,
21
22
  )
22
23
 
23
24
  code_rules_enforcer = SimpleNamespace(
24
25
  changed_line_numbers=changed_line_numbers,
26
+ docstring_line_numbers=docstring_line_numbers,
25
27
  prior_and_post_edit_content=prior_and_post_edit_content,
26
28
  )
27
29
 
28
30
 
31
+ def test_docstring_line_numbers_spans_a_multi_line_function_docstring() -> None:
32
+ source = (
33
+ "def describe():\n"
34
+ ' """First row.\n'
35
+ "\n"
36
+ " Third row.\n"
37
+ ' """\n'
38
+ " return 1\n"
39
+ )
40
+ assert code_rules_enforcer.docstring_line_numbers(source) == {2, 3, 4, 5}
41
+
42
+
43
+ def test_docstring_line_numbers_excludes_real_code_lines() -> None:
44
+ source = "def compute():\n counter = 41\n return counter\n"
45
+ assert code_rules_enforcer.docstring_line_numbers(source) == set()
46
+
47
+
48
+ def test_docstring_line_numbers_returns_empty_set_on_syntax_error() -> None:
49
+ assert code_rules_enforcer.docstring_line_numbers("def broken(:\n") == set()
50
+
51
+
29
52
  def test_readable_prior_yields_consistent_prior_and_reconstruction(tmp_path) -> None:
30
53
  """When the prior reads cleanly, the helper returns the same prior content it
31
54
  reconstructed the post-edit view from, so the two never diverge across two