claude-dev-env 1.83.0 → 1.85.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/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -4
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +0 -4
- package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +133 -19
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_imports_logging.py +10 -6
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +15 -8
- package/hooks/blocking/code_rules_paired_test.py +3 -2
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +1 -1
- package/hooks/blocking/code_rules_unused_imports.py +2 -2
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +24 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +4 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/env-var-table-code-drift.md +1 -1
- package/rules/package-inventory-stale-entry.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/_shared/pr-loop/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
- package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
- package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
- package/skills/autoconverge/SKILL.md +82 -58
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +71 -74
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +99 -60
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +27 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -160,6 +160,24 @@ def test_path_contains_glob_metacharacters_local_tuple_uses_all_collection_prefi
|
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def test_is_valid_project_root_detects_git_marker(tmp_path: Path) -> None:
|
|
164
|
+
git_project_root = tmp_path / "git_project"
|
|
165
|
+
(git_project_root / ".git").mkdir(parents=True)
|
|
166
|
+
assert common.is_valid_project_root(git_project_root) is True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_is_valid_project_root_detects_claude_marker(tmp_path: Path) -> None:
|
|
170
|
+
claude_project_root = tmp_path / "claude_project"
|
|
171
|
+
(claude_project_root / ".claude").mkdir(parents=True)
|
|
172
|
+
assert common.is_valid_project_root(claude_project_root) is True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_is_valid_project_root_rejects_unmarked_directory(tmp_path: Path) -> None:
|
|
176
|
+
unmarked_directory = tmp_path / "no_marker"
|
|
177
|
+
unmarked_directory.mkdir()
|
|
178
|
+
assert common.is_valid_project_root(unmarked_directory) is False
|
|
179
|
+
|
|
180
|
+
|
|
163
181
|
def test_is_valid_project_root_uses_extracted_directory_marker_constants() -> None:
|
|
164
182
|
"""is_valid_project_root must reference extracted constants, not inline string literals."""
|
|
165
183
|
source_text = inspect.getsource(common.is_valid_project_root)
|
|
@@ -9,6 +9,7 @@ Covers:
|
|
|
9
9
|
|
|
10
10
|
import importlib.util
|
|
11
11
|
import inspect
|
|
12
|
+
import os
|
|
12
13
|
import subprocess
|
|
13
14
|
import sys
|
|
14
15
|
import unittest.mock
|
|
@@ -93,6 +94,13 @@ def stage_file(repository_root: Path, relative_path: str) -> None:
|
|
|
93
94
|
run_git_in_repository(repository_root, "add", "--", relative_path)
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
@pytest.fixture(autouse=True)
|
|
98
|
+
def isolated_git_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
99
|
+
for each_variable_name in list(os.environ):
|
|
100
|
+
if each_variable_name.startswith("GIT_"):
|
|
101
|
+
monkeypatch.delenv(each_variable_name, raising=False)
|
|
102
|
+
|
|
103
|
+
|
|
96
104
|
@pytest.fixture()
|
|
97
105
|
def temporary_git_repository(tmp_path: Path) -> Path:
|
|
98
106
|
repository_root = tmp_path / "repository_under_test"
|
|
@@ -320,6 +328,35 @@ def test_main_staged_mode_exits_zero_when_nothing_staged(
|
|
|
320
328
|
assert exit_code == 0
|
|
321
329
|
|
|
322
330
|
|
|
331
|
+
def test_main_staged_mode_passes_when_only_terminology_findings_exist(
|
|
332
|
+
temporary_git_repository: Path,
|
|
333
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
334
|
+
) -> None:
|
|
335
|
+
write_file(temporary_git_repository / "quota.py", "first_count = 1\n")
|
|
336
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
337
|
+
write_file(
|
|
338
|
+
temporary_git_repository / "quota.py",
|
|
339
|
+
"first_count = 1\npremium_interactions = 1\n",
|
|
340
|
+
)
|
|
341
|
+
write_file(
|
|
342
|
+
temporary_git_repository / "README.md",
|
|
343
|
+
"The premium-request budget gates the run.\n",
|
|
344
|
+
)
|
|
345
|
+
stage_file(temporary_git_repository, "quota.py")
|
|
346
|
+
stage_file(temporary_git_repository, "README.md")
|
|
347
|
+
|
|
348
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
349
|
+
terminology_findings = gate_module.staged_terminology_findings(
|
|
350
|
+
temporary_git_repository
|
|
351
|
+
)
|
|
352
|
+
exit_code = gate_module.main(["--staged"])
|
|
353
|
+
|
|
354
|
+
assert any(
|
|
355
|
+
"premium-request" in each_finding for each_finding in terminology_findings
|
|
356
|
+
)
|
|
357
|
+
assert exit_code == 0
|
|
358
|
+
|
|
359
|
+
|
|
323
360
|
def test_added_lines_for_staged_file_returns_empty_for_modified_file_with_no_additions(
|
|
324
361
|
temporary_git_repository: Path,
|
|
325
362
|
) -> None:
|
|
@@ -120,3 +120,17 @@ def test_cli_main_supports_bugteam_reviewer(
|
|
|
120
120
|
) -> None:
|
|
121
121
|
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
|
|
122
122
|
assert reviews_disabled.main(["--reviewer", "bugteam"]) == 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_cli_main_supports_copilot_reviewer(
|
|
126
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
127
|
+
) -> None:
|
|
128
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
|
|
129
|
+
assert reviews_disabled.main(["--reviewer", "copilot"]) == 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_cli_main_returns_one_for_copilot_when_only_bugbot_disabled(
|
|
133
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
134
|
+
) -> None:
|
|
135
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
|
|
136
|
+
assert reviews_disabled.main(["--reviewer", "copilot"]) == 1
|
|
@@ -23,13 +23,22 @@ main = sweep_module.main
|
|
|
23
23
|
parse_added_lines = sweep_module._parse_added_lines
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
_git_environment = sweep_module._git_environment
|
|
27
|
+
|
|
28
|
+
|
|
26
29
|
def _init_git_repository(repository_path: Path) -> None:
|
|
27
30
|
for each_command in (
|
|
28
31
|
["git", "init"],
|
|
29
32
|
["git", "config", "user.email", "test@example.com"],
|
|
30
33
|
["git", "config", "user.name", "Test"],
|
|
31
34
|
):
|
|
32
|
-
subprocess.run(
|
|
35
|
+
subprocess.run(
|
|
36
|
+
each_command,
|
|
37
|
+
cwd=repository_path,
|
|
38
|
+
check=True,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
env=_git_environment(),
|
|
41
|
+
)
|
|
33
42
|
|
|
34
43
|
|
|
35
44
|
CODE_AND_PROSE_DIFF = (
|
|
@@ -220,17 +229,86 @@ def test_does_not_flag_ordinary_sentence_sharing_loop_variable_prefix() -> None:
|
|
|
220
229
|
|
|
221
230
|
|
|
222
231
|
def test_flags_near_miss_inside_code_comment() -> None:
|
|
232
|
+
diff = (
|
|
233
|
+
"diff --git a/api/quota.mjs b/api/quota.mjs\n"
|
|
234
|
+
"--- a/api/quota.mjs\n"
|
|
235
|
+
"+++ b/api/quota.mjs\n"
|
|
236
|
+
"@@ -0,0 +1,2 @@\n"
|
|
237
|
+
"+const premium_interactions = 5\n"
|
|
238
|
+
"+// the premium-request path resets the counter\n"
|
|
239
|
+
)
|
|
240
|
+
findings = sweep_diff(diff)
|
|
241
|
+
assert len(findings) == 1
|
|
242
|
+
assert "premium-request" in findings[0]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_flags_near_miss_inside_code_string_literal() -> None:
|
|
246
|
+
diff = (
|
|
247
|
+
"diff --git a/api/quota.mjs b/api/quota.mjs\n"
|
|
248
|
+
"--- a/api/quota.mjs\n"
|
|
249
|
+
"+++ b/api/quota.mjs\n"
|
|
250
|
+
"@@ -0,0 +1,2 @@\n"
|
|
251
|
+
"+const premium_interactions = 5\n"
|
|
252
|
+
'+throw new Error("the premium-request budget")\n'
|
|
253
|
+
)
|
|
254
|
+
findings = sweep_diff(diff)
|
|
255
|
+
assert len(findings) == 1
|
|
256
|
+
assert "premium-request" in findings[0]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_does_not_flag_prose_inside_code_string_literal() -> None:
|
|
260
|
+
diff = (
|
|
261
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
262
|
+
"--- a/api/quota.py\n"
|
|
263
|
+
"+++ b/api/quota.py\n"
|
|
264
|
+
"@@ -0,0 +1,2 @@\n"
|
|
265
|
+
"+premium_interactions = 5\n"
|
|
266
|
+
'+message = "the premium-request budget"\n'
|
|
267
|
+
)
|
|
268
|
+
assert sweep_diff(diff) == []
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_does_not_flag_prose_inside_code_docstring() -> None:
|
|
223
272
|
diff = (
|
|
224
273
|
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
225
274
|
"--- a/api/quota.py\n"
|
|
226
275
|
"+++ b/api/quota.py\n"
|
|
227
276
|
"@@ -0,0 +1,2 @@\n"
|
|
228
277
|
"+premium_interactions = 5\n"
|
|
229
|
-
"
|
|
278
|
+
'+ """Gate the run on the premium-request budget."""\n'
|
|
279
|
+
)
|
|
280
|
+
assert sweep_diff(diff) == []
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_does_not_flag_prose_in_test_file() -> None:
|
|
284
|
+
diff = (
|
|
285
|
+
"diff --git a/api/quota.test.mjs b/api/quota.test.mjs\n"
|
|
286
|
+
"--- a/api/quota.test.mjs\n"
|
|
287
|
+
"+++ b/api/quota.test.mjs\n"
|
|
288
|
+
"@@ -0,0 +1,2 @@\n"
|
|
289
|
+
"+const premium_interactions = 5\n"
|
|
290
|
+
"+// the premium-request path resets the counter\n"
|
|
291
|
+
)
|
|
292
|
+
assert sweep_diff(diff) == []
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_flags_prose_in_test_named_markdown_doc() -> None:
|
|
296
|
+
diff = (
|
|
297
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
298
|
+
"--- a/api/quota.py\n"
|
|
299
|
+
"+++ b/api/quota.py\n"
|
|
300
|
+
"@@ -0,0 +1,1 @@\n"
|
|
301
|
+
"+premium_interactions = 5\n"
|
|
302
|
+
"diff --git a/docs/test_plan.md b/docs/test_plan.md\n"
|
|
303
|
+
"--- a/docs/test_plan.md\n"
|
|
304
|
+
"+++ b/docs/test_plan.md\n"
|
|
305
|
+
"@@ -0,0 +1,1 @@\n"
|
|
306
|
+
"+The premium-request budget gates the run.\n"
|
|
230
307
|
)
|
|
231
308
|
findings = sweep_diff(diff)
|
|
232
309
|
assert len(findings) == 1
|
|
233
310
|
assert "premium-request" in findings[0]
|
|
311
|
+
assert "test_plan.md" in findings[0]
|
|
234
312
|
|
|
235
313
|
|
|
236
314
|
def test_no_findings_when_no_multiword_identifier_introduced() -> None:
|
|
@@ -273,7 +351,13 @@ def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
|
|
|
273
351
|
(tmp_path / "README.md").write_text(
|
|
274
352
|
"The premium-request budget gates the run.\n", encoding="utf-8"
|
|
275
353
|
)
|
|
276
|
-
subprocess.run(
|
|
354
|
+
subprocess.run(
|
|
355
|
+
["git", "add", "-A"],
|
|
356
|
+
cwd=tmp_path,
|
|
357
|
+
check=True,
|
|
358
|
+
capture_output=True,
|
|
359
|
+
env=_git_environment(),
|
|
360
|
+
)
|
|
277
361
|
findings = staged_terminology_findings(tmp_path)
|
|
278
362
|
assert any("premium-request" in each_finding for each_finding in findings)
|
|
279
363
|
|
|
@@ -281,7 +365,13 @@ def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
|
|
|
281
365
|
def test_staged_terminology_findings_empty_when_clean(tmp_path: Path) -> None:
|
|
282
366
|
_init_git_repository(tmp_path)
|
|
283
367
|
(tmp_path / "README.md").write_text("Nothing notable here.\n", encoding="utf-8")
|
|
284
|
-
subprocess.run(
|
|
368
|
+
subprocess.run(
|
|
369
|
+
["git", "add", "-A"],
|
|
370
|
+
cwd=tmp_path,
|
|
371
|
+
check=True,
|
|
372
|
+
capture_output=True,
|
|
373
|
+
env=_git_environment(),
|
|
374
|
+
)
|
|
285
375
|
assert staged_terminology_findings(tmp_path) == []
|
|
286
376
|
|
|
287
377
|
|
package/agents/clean-coder.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clean-coder
|
|
3
3
|
description: "Use PROACTIVELY for ALL code generation — feature development, bug fixes, refactoring, hook creation, automation scripts, and any task that produces code. Internalizes CODE_RULES.md and the 8-dimension readability standard so thoroughly that /check finds zero issues. The definitive code-writing agent."
|
|
4
|
+
model: opus
|
|
4
5
|
tools: Read, Write, Edit, Bash, Grep, Glob, Task, Skill
|
|
5
6
|
color: green
|
|
6
7
|
---
|
|
@@ -436,6 +437,8 @@ This default is overridden by explicit user instruction such as "refactor this e
|
|
|
436
437
|
|
|
437
438
|
Docstrings on functions, methods, classes, and modules are encouraged for public APIs. The self-documenting-names gate inspects inline `#` and block `#` comments only; docstrings are exempt from that gate.
|
|
438
439
|
|
|
440
|
+
Write a behavior-illustrating docstring in the diagram-first shape: one summary line, then a `::` literal block or a doctest that shows a concrete input and its marked outcome (with `ok:` / `flag:` contrast lines where a pass-and-fail pair helps), then a couple of short neutral prose lines, then the `Args:` / `Returns:` sections. Two hooks back this: `check_docstring_runon_sentence` flags a run-on prose wall, and `check_docstring_prose_wall_without_illustration` flags a narrative that runs long with no diagram. See `../rules/plain-illustrative-docstrings.md` for the shape and a worked example.
|
|
441
|
+
|
|
439
442
|
## Audit Awareness
|
|
440
443
|
|
|
441
444
|
Code clean-coder writes will be audited later against the A–P bug categories from `code-quality-agent`. The hooks listed in this file enforce the Category J slice at write time, but A–I and K–P surface only in audit. For each category's full rubric, sub-bucket decomposition, and concrete checks, see `../audit-rubrics/category_rubrics/` (relative to this agent file). While generating code, anticipate the full A–P surface so the first write clears every audit category.
|
|
@@ -28,7 +28,7 @@ Decomposition is by the **kind of docstring claim** that needs to be cross-check
|
|
|
28
28
|
| O6 | Free-form `Args:`-adjacent claims | A docstring's `Returns:` / `Raises:` / `Note:` / `Example:` sections make claims (`returns shared-temp only`, `raises ValueError on missing key`). Verify each claim against the body. When a docstring enumerates the inputs a body counts (a "field counts as read when ..." list, a list of conditions treated as a match, a list of cases the body skips), list every union member and every suppressor the body applies (`read_names = a | b | c`, each early-return guard) and confirm each appears in the prose enumeration. A union member or suppressor the body applies but the prose omits is an O6 finding. When a docstring sentence excludes a named category of input from what the function flags (`X are not dispatch steps`, `Y is not a match`), confirm the axis the prose excludes on is the axis the body's branch condition actually keys on. A body that flags a call when it sits inside an `If.test` guard, paired with prose that excludes by the call's receiver shape (`method-on-local calls inside a branch are not dispatch steps`), is an O6 finding: a guarded method-on-local call is flagged even though the prose lists it as excluded — the exclusion is keyed to the wrong axis. The single-condition shared-fallback shape of this drift — a summary that scopes a fallback call to one condition while the body routes to that same call from two or more early-return guards — is gated deterministically at Write/Edit time by `check_docstring_fallback_branch_coverage`, so the audit lane focuses on the O6 shapes the gate cannot match. The exception-guard shape of this drift — a docstring that promises a malformed payload `resolves to None` while a payload subscript (`payload["key"]`, `float(payload["key"])`) sits outside the try/except whose handler returns None, so a present-but-malformed payload raises rather than resolving to None — is gated deterministically at Write/Edit time by `check_docstring_unguarded_malformed_payload_claim`, so the audit lane focuses on the wider Raises/None-on-failure claims the gate cannot match. A `Returns:` that names the mechanism, tool, or output format the function produces (`instructing a StructuredOutput summary`, `returns a YAML document`, `emits a JSON object`) matches the artifact the body actually builds: a prompt body that asks the agent to "Return strictly a JSON object" while the docstring claims it "instruct[s] a StructuredOutput" summary is an O6 finding, because the named tool appears nowhere in the emitted text. See `../../rules/docstring-prose-matches-implementation.md`. |
|
|
29
29
|
| O7 | Module-doc-vs-split-module after refactor | When a refactor moves a responsibility to a sibling module, the originating module's docstring and the receiving module's docstring both describe the home of that responsibility. A module docstring should describe only the responsibilities it owns. |
|
|
30
30
|
| O8 | Companion-doc ordering/content vs producer | When a PR changes a producer function's ordering or union, read that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact (a file path, a JSON key, a named list). A doc sentence that claims the artifact is `sorted` / `alphabetical` / `in sorted order`, or holds `just the at-risk names` / `only the current set`, while the producer merges stored names with new names and appends — preserving file order, not re-sorting the union — is an O8 finding on both counts (wrong order claim, hidden merged-in entries). The finding stands even when the PR diff never touched the `.md` file, because the behavior change orphaned the doc claim. See `../../rules/docstring-prose-matches-implementation.md`. |
|
|
31
|
-
| O9 | Python docstring plainness for a general developer | A changed module / class / public-function docstring's narrative prose — the summary and description before the first `Args:` / `Returns:` / `Raises:` / `Yields:` section — reads plainly and paints a concrete scene a general developer follows on first read. Flag a narrative that stacks abstract machinery nouns into a wall (`the SIGINT install/restore/installability check, the atexit terminal-record registration, and the interrupted-run finalizer`), that defines a thing by what it is not (`the non-promoter-specific machinery`), or that runs one sentence long while joining clauses with an em-dash or a semicolon. The deterministic run-on mark is gated at Write/Edit time by `check_docstring_runon_sentence` in `code_rules_docstrings.py`, so this lane carries the
|
|
31
|
+
| O9 | Python docstring plainness for a general developer | A changed module / class / public-function docstring's narrative prose — the summary and description before the first `Args:` / `Returns:` / `Raises:` / `Yields:` section — reads plainly and paints a concrete scene a general developer follows on first read. Flag a narrative that stacks abstract machinery nouns into a wall (`the SIGINT install/restore/installability check, the atexit terminal-record registration, and the interrupted-run finalizer`), that defines a thing by what it is not (`the non-promoter-specific machinery`), or that runs one sentence long while joining clauses with an em-dash or a semicolon. The diagram-first shape carries this best: a summary line, then a `::` example block or a doctest that shows a concrete input and its marked outcome, then a couple of short prose lines. The deterministic run-on mark is gated at Write/Edit time by `check_docstring_runon_sentence` in `code_rules_docstrings.py`, and a narrative that runs more than six prose lines with no such block is gated by `check_docstring_prose_wall_without_illustration` in the same module, so this lane carries the judgment the gates cannot: whether a stranger to the code pictures the moment, the input, and the outcome after one read, and whether the diagram truly illustrates — a real input, a marked outcome, an `ok:` / `flag:` contrast a reader learns from. See `../../rules/plain-illustrative-docstrings.md`. |
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
package/bin/install.mjs
CHANGED
|
@@ -294,10 +294,13 @@ function backupClaudeHubBeforeOverwrite(destPath, incomingPath) {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
297
|
+
* PreToolUse hook script paths the installer manages even though hooks.json
|
|
298
|
+
* carries no standalone entry for them. Most were folded into the PreToolUse
|
|
299
|
+
* dispatcher in Stage 1; md_to_html_blocker is a retired hook with no script on
|
|
300
|
+
* disk. Each path stays in this set so a reinstall from an older settings shape
|
|
301
|
+
* prunes its standalone entry — a folded hook would otherwise double-run
|
|
302
|
+
* alongside the dispatcher, and the retired hook's entry would point at a
|
|
303
|
+
* missing script.
|
|
301
304
|
*/
|
|
302
305
|
export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
303
306
|
'blocking/write_existing_file_blocker.py',
|
|
@@ -316,18 +319,23 @@ export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
|
316
319
|
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
317
320
|
'blocking/open_questions_in_plans_blocker.py',
|
|
318
321
|
'blocking/plain_language_blocker.py',
|
|
322
|
+
'blocking/md_to_html_blocker.py',
|
|
319
323
|
]);
|
|
320
324
|
|
|
321
325
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
+
* After-write hook script paths the installer manages even though hooks.json
|
|
327
|
+
* carries no standalone entry for them. mypy_validator and auto_formatter run
|
|
328
|
+
* hosted inside the PostToolUse dispatcher; doc_gist_auto_publish and
|
|
329
|
+
* md_to_html_companion are retired hooks with no script on disk. Each path stays
|
|
330
|
+
* in this set so a reinstall from an older settings shape prunes its standalone
|
|
331
|
+
* entry — a hosted hook would otherwise double-run alongside the dispatcher, and
|
|
332
|
+
* a retired hook's entry would point at a missing script.
|
|
326
333
|
*/
|
|
327
334
|
export const POST_FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
328
335
|
'validation/mypy_validator.py',
|
|
329
336
|
'workflow/auto_formatter.py',
|
|
330
337
|
'workflow/doc_gist_auto_publish.py',
|
|
338
|
+
'workflow/md_to_html_companion.py',
|
|
331
339
|
]);
|
|
332
340
|
|
|
333
341
|
/**
|
package/bin/install.test.mjs
CHANGED
|
@@ -718,12 +718,13 @@ const OLD_FOLDED_HOOKS_SETTINGS = {
|
|
|
718
718
|
};
|
|
719
719
|
|
|
720
720
|
|
|
721
|
-
test('FOLDED_HOOK_RELATIVE_PATHS contains all 16 hooks removed from hooks.json', () => {
|
|
722
|
-
assert.equal(FOLDED_HOOK_RELATIVE_PATHS.size,
|
|
721
|
+
test('FOLDED_HOOK_RELATIVE_PATHS contains all 16 hooks removed from hooks.json plus the retired md_to_html_blocker', () => {
|
|
722
|
+
assert.equal(FOLDED_HOOK_RELATIVE_PATHS.size, 17);
|
|
723
723
|
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/write_existing_file_blocker.py'));
|
|
724
724
|
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/plain_language_blocker.py'));
|
|
725
725
|
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/code_rules_enforcer.py'));
|
|
726
726
|
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/pytest_testpaths_orphan_blocker.py'));
|
|
727
|
+
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/md_to_html_blocker.py'));
|
|
727
728
|
});
|
|
728
729
|
|
|
729
730
|
|
|
@@ -746,16 +747,25 @@ test('FOLDED_HOOK_RELATIVE_PATHS lists every hook the PreToolUse dispatcher host
|
|
|
746
747
|
'blocking/open_questions_in_plans_blocker.py',
|
|
747
748
|
'blocking/plain_language_blocker.py',
|
|
748
749
|
];
|
|
750
|
+
const retiredHooks = [
|
|
751
|
+
'blocking/md_to_html_blocker.py',
|
|
752
|
+
];
|
|
749
753
|
for (const hostedPath of dispatcherHostedHooks) {
|
|
750
754
|
assert.ok(
|
|
751
755
|
FOLDED_HOOK_RELATIVE_PATHS.has(hostedPath),
|
|
752
756
|
`dispatcher-hosted hook ${hostedPath} must be in FOLDED_HOOK_RELATIVE_PATHS so a reinstall prunes its stale standalone entry and it does not double-run`
|
|
753
757
|
);
|
|
754
758
|
}
|
|
759
|
+
for (const retiredPath of retiredHooks) {
|
|
760
|
+
assert.ok(
|
|
761
|
+
FOLDED_HOOK_RELATIVE_PATHS.has(retiredPath),
|
|
762
|
+
`retired hook ${retiredPath} must be in FOLDED_HOOK_RELATIVE_PATHS so a reinstall prunes its stale standalone entry pointing at a script no longer on disk`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
755
765
|
assert.equal(
|
|
756
766
|
FOLDED_HOOK_RELATIVE_PATHS.size,
|
|
757
|
-
dispatcherHostedHooks.length,
|
|
758
|
-
'FOLDED_HOOK_RELATIVE_PATHS must hold exactly the dispatcher-hosted hooks, no more, no fewer'
|
|
767
|
+
dispatcherHostedHooks.length + retiredHooks.length,
|
|
768
|
+
'FOLDED_HOOK_RELATIVE_PATHS must hold exactly the dispatcher-hosted hooks plus the retired hooks, no more, no fewer'
|
|
759
769
|
);
|
|
760
770
|
});
|
|
761
771
|
|
|
@@ -887,11 +897,12 @@ const OLD_POST_FOLDED_HOOKS_SETTINGS = {
|
|
|
887
897
|
};
|
|
888
898
|
|
|
889
899
|
|
|
890
|
-
test('POST_FOLDED_HOOK_RELATIVE_PATHS contains the
|
|
891
|
-
assert.equal(POST_FOLDED_HOOK_RELATIVE_PATHS.size,
|
|
900
|
+
test('POST_FOLDED_HOOK_RELATIVE_PATHS contains the after-write hooks and the retired md_to_html_companion', () => {
|
|
901
|
+
assert.equal(POST_FOLDED_HOOK_RELATIVE_PATHS.size, 4);
|
|
892
902
|
assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('validation/mypy_validator.py'));
|
|
893
903
|
assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('workflow/auto_formatter.py'));
|
|
894
904
|
assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('workflow/doc_gist_auto_publish.py'));
|
|
905
|
+
assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('workflow/md_to_html_companion.py'));
|
|
895
906
|
});
|
|
896
907
|
|
|
897
908
|
|
package/commands/CLAUDE.md
CHANGED
|
@@ -7,7 +7,6 @@ Slash-command definitions installed into `~/.claude/commands/` by `bin/install.m
|
|
|
7
7
|
| File | Command | What it does |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| `commit.md` | `/commit` | Commits and pushes changes to GitHub |
|
|
10
|
-
| `doc-gist.md` | `/doc-gist` | Uploads an HTML file as a secret gist and opens the htmlpreview URL |
|
|
11
10
|
| `docupdate.md` | `/docupdate` | Updates documentation to match current code state |
|
|
12
11
|
| `hook-log-extract.md` | `/hook-log-extract` | Extracts and formats hook log entries for a session |
|
|
13
12
|
| `hook-log-init.md` | `/hook-log-init` | Initializes the Neon Postgres schema that backs the hook-log extractor (one-time per machine) |
|
package/docs/CODE_RULES.md
CHANGED
|
@@ -94,4 +94,4 @@ If you already have the data, don't fetch it again.
|
|
|
94
94
|
|
|
95
95
|
## 11. ENFORCEMENT SURFACES
|
|
96
96
|
|
|
97
|
-
⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, conservative-action, BDD discovery, docstring-prose-matches-implementation; the `/code` skill prepends strict mode for a session: no `Any`/`cast()`, immutable TypedDicts with `_encode_*`/`_decode_*` + `require_*` validation, per-module `_test_hooks.py` DI, 100% statement + branch coverage, zero mocks). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–Q) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md` and `skills/code/SKILL.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit.
|
|
97
|
+
⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, conservative-action, BDD discovery, docstring-prose-matches-implementation; the `/code` skill prepends strict mode for a session: no `Any`/`cast()`, immutable TypedDicts with `_encode_*`/`_decode_*` + `require_*` validation, per-module `_test_hooks.py` DI, 100% statement + branch coverage, zero mocks). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–Q) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md` and `skills/code/SKILL.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit. The diagram-first docstring standard (a summary line, then a `::` example or doctest, then a couple of short prose lines) lives in `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`, enforced by the `check_docstring_runon_sentence` and `check_docstring_prose_wall_without_illustration` backstop hooks and Category O9 audit.
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -77,7 +77,6 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
77
77
|
| `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`) |
|
|
78
78
|
| `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on |
|
|
79
79
|
| `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work |
|
|
80
|
-
| `md_to_html_blocker.py` | PreToolUse (Write/Edit) | Writing `.md` files when an `.html` companion is required |
|
|
81
80
|
| `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
|
|
82
81
|
| `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
|
|
83
82
|
| `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
|
|
@@ -107,8 +106,6 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
107
106
|
| File | Role |
|
|
108
107
|
|---|---|
|
|
109
108
|
| `_gh_body_arg_utils.py` | Parsing helpers for `gh_body_arg_blocker.py` |
|
|
110
|
-
| `_md_to_html_blocker_test_support.py` | Test fixtures shared across `md_to_html_blocker` tests |
|
|
111
|
-
| `md_path_exemptions.py` | Path exemption logic for `md_to_html_blocker.py` |
|
|
112
109
|
| `pr_description_body_audit.py` | Body audit logic for `pr_description_enforcer.py` |
|
|
113
110
|
| `pr_description_command_parser.py` | `gh` command parsing for `pr_description_enforcer.py` |
|
|
114
111
|
| `pr_description_pr_number.py` | PR number extraction logic |
|
|
@@ -119,4 +116,3 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
119
116
|
|
|
120
117
|
- Tests live beside each hook as `test_<hookname>.py` or `test_<hookname>_<suffix>.py`. Run with `python -m pytest <test_file>`.
|
|
121
118
|
- Tunable constants live in `hooks_constants/<hook_name>_constants.py`; the verified-commit family uses `blocking/config/verified_commit_constants.py`.
|
|
122
|
-
- `conftest.py` gives shared pytest fixtures for the blocking test suite.
|
|
@@ -45,11 +45,11 @@ from hooks_constants.claude_md_orphan_file_blocker_constants import ( # noqa: E
|
|
|
45
45
|
SEPARATOR_CELL_PATTERN,
|
|
46
46
|
TABLE_ROW_PATTERN,
|
|
47
47
|
)
|
|
48
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
48
49
|
from hooks_constants.multi_edit_reconstruction import ( # noqa: E402
|
|
49
50
|
apply_edits,
|
|
50
51
|
edits_for_tool,
|
|
51
52
|
)
|
|
52
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
53
53
|
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
54
54
|
read_hook_input_dictionary_from_stdin,
|
|
55
55
|
)
|
|
@@ -23,6 +23,7 @@ from code_rules_path_utils import ( # noqa: E402
|
|
|
23
23
|
)
|
|
24
24
|
from code_rules_shared import ( # noqa: E402
|
|
25
25
|
_build_parent_map,
|
|
26
|
+
docstring_line_numbers,
|
|
26
27
|
get_file_extension,
|
|
27
28
|
is_migration_file,
|
|
28
29
|
is_test_file,
|
|
@@ -110,6 +111,7 @@ def _scan_function_body_constants(content: str) -> list[str]:
|
|
|
110
111
|
"""
|
|
111
112
|
advisory_issues: list[str] = []
|
|
112
113
|
lines = content.split("\n")
|
|
114
|
+
all_docstring_line_numbers = docstring_line_numbers(content)
|
|
113
115
|
function_indent_stack: list[int] = []
|
|
114
116
|
constant_pattern = re.compile(r"^([A-Z][A-Z0-9_]{2,})(?:\s*:\s*[^=]+)?\s*=\s*[^=]")
|
|
115
117
|
|
|
@@ -119,6 +121,9 @@ def _scan_function_body_constants(content: str) -> list[str]:
|
|
|
119
121
|
if not stripped:
|
|
120
122
|
continue
|
|
121
123
|
|
|
124
|
+
if each_line_number in all_docstring_line_numbers:
|
|
125
|
+
continue
|
|
126
|
+
|
|
122
127
|
indent = len(each_line) - len(each_line.lstrip())
|
|
123
128
|
|
|
124
129
|
while function_indent_stack and indent <= function_indent_stack[-1] and not stripped.startswith(("#", "@", ")")):
|